mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-08-30 11:21:40 +00:00
174 lines
5.9 KiB
TypeScript
174 lines
5.9 KiB
TypeScript
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
|
|
import {
|
|
SpaceDetailTabsNav,
|
|
type SpaceDetailTab,
|
|
} from '@/components/Home/SpaceDetailTabsNav';
|
|
import {
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from '@testing-library/react';
|
|
import { useState } from 'react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const motionMocks = vi.hoisted(() => ({ reduced: false }));
|
|
|
|
vi.mock('framer-motion', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('framer-motion')>();
|
|
return {
|
|
...actual,
|
|
useReducedMotion: () => motionMocks.reduced,
|
|
};
|
|
});
|
|
|
|
function TabsHarness() {
|
|
const [activeTab, setActiveTab] = useState<SpaceDetailTab>('projects');
|
|
return <SpaceDetailTabsNav activeTab={activeTab} onChange={setActiveTab} />;
|
|
}
|
|
|
|
describe('SpaceDetailTabsNav', () => {
|
|
beforeEach(() => {
|
|
motionMocks.reduced = false;
|
|
class TestPointerEvent extends MouseEvent {
|
|
readonly pointerType: string;
|
|
|
|
constructor(type: string, eventInit: PointerEventInit = {}) {
|
|
super(type, eventInit);
|
|
this.pointerType = eventInit.pointerType || '';
|
|
}
|
|
}
|
|
vi.stubGlobal('PointerEvent', TestPointerEvent);
|
|
vi.spyOn(globalThis, 'requestAnimationFrame').mockImplementation(
|
|
(callback) => {
|
|
callback(0);
|
|
return 1;
|
|
}
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('preserves tab roles, roving focus, and instant keyboard navigation', () => {
|
|
render(<TabsHarness />);
|
|
|
|
const tablist = screen.getByRole('tablist', { name: 'Space content' });
|
|
const tabs = within(tablist).getAllByRole('tab');
|
|
expect(tabs).toHaveLength(6);
|
|
expect(screen.getByRole('tab', { name: 'Projects' })).toHaveAttribute(
|
|
'aria-selected',
|
|
'true'
|
|
);
|
|
|
|
fireEvent.keyDown(screen.getByRole('tab', { name: 'Projects' }), {
|
|
key: 'ArrowRight',
|
|
});
|
|
expect(screen.getByRole('tab', { name: 'Tasks' })).toHaveAttribute(
|
|
'aria-selected',
|
|
'true'
|
|
);
|
|
expect(screen.getByRole('tab', { name: 'Tasks' })).toHaveFocus();
|
|
expect(tablist).toHaveAttribute('data-layout-movement', 'instant');
|
|
|
|
fireEvent.keyDown(screen.getByRole('tab', { name: 'Tasks' }), {
|
|
key: 'End',
|
|
});
|
|
expect(screen.getByRole('tab', { name: 'Space Settings' })).toHaveFocus();
|
|
|
|
fireEvent.keyDown(screen.getByRole('tab', { name: 'Space Settings' }), {
|
|
key: 'Home',
|
|
});
|
|
expect(screen.getByRole('tab', { name: 'Projects' })).toHaveFocus();
|
|
});
|
|
|
|
it('uses instant Enter and Space activation until pointer interaction restores the spring', () => {
|
|
render(<TabsHarness />);
|
|
const tablist = screen.getByRole('tablist', { name: 'Space content' });
|
|
const tasksTab = screen.getByRole('tab', { name: 'Tasks' });
|
|
const memoryTab = screen.getByRole('tab', { name: 'Memory' });
|
|
|
|
fireEvent.keyDown(tasksTab, { key: 'Enter' });
|
|
fireEvent.click(tasksTab);
|
|
expect(tasksTab).toHaveAttribute('aria-selected', 'true');
|
|
expect(tablist).toHaveAttribute('data-layout-movement', 'instant');
|
|
|
|
fireEvent.keyDown(memoryTab, { key: ' ' });
|
|
fireEvent.click(memoryTab);
|
|
expect(memoryTab).toHaveAttribute('aria-selected', 'true');
|
|
expect(tablist).toHaveAttribute('data-layout-movement', 'instant');
|
|
|
|
fireEvent.pointerDown(tasksTab, { pointerType: 'mouse' });
|
|
fireEvent.click(tasksTab);
|
|
expect(tasksTab).toHaveAttribute('aria-selected', 'true');
|
|
expect(tablist).toHaveAttribute('data-layout-movement', 'spring');
|
|
});
|
|
|
|
it('owns shared surfaces inside their tabs and clears hover for touch pointers', async () => {
|
|
const { container } = render(<TabsHarness />);
|
|
const projectsTab = screen.getByRole('tab', { name: 'Projects' });
|
|
const memoryTab = screen.getByRole('tab', { name: 'Memory' });
|
|
|
|
expect(
|
|
projectsTab.querySelector('[data-space-detail-tab-indicator]')
|
|
).toBeInTheDocument();
|
|
fireEvent.pointerEnter(memoryTab, { pointerType: 'mouse' });
|
|
expect(
|
|
memoryTab.querySelector('[data-space-detail-tab-hover]')
|
|
).toBeInTheDocument();
|
|
|
|
fireEvent.pointerEnter(projectsTab, { pointerType: 'touch' });
|
|
await waitFor(() =>
|
|
expect(
|
|
container.querySelector('[data-space-detail-tab-hover]')
|
|
).not.toBeInTheDocument()
|
|
);
|
|
|
|
fireEvent.pointerEnter(memoryTab, { pointerType: 'mouse' });
|
|
expect(
|
|
memoryTab.querySelector('[data-space-detail-tab-hover]')
|
|
).toBeInTheDocument();
|
|
fireEvent.pointerDown(projectsTab, { pointerType: 'touch' });
|
|
await waitFor(() =>
|
|
expect(
|
|
container.querySelector('[data-space-detail-tab-hover]')
|
|
).not.toBeInTheDocument()
|
|
);
|
|
|
|
const indicator = container.querySelector(
|
|
'[data-space-detail-tab-indicator]'
|
|
) as HTMLElement;
|
|
expect(indicator.style.left).toBe('');
|
|
expect(indicator.style.top).toBe('');
|
|
expect(indicator.style.width).toBe('');
|
|
expect(indicator.style.height).toBe('');
|
|
});
|
|
|
|
it('selects the instant layout branch for reduced motion', () => {
|
|
motionMocks.reduced = true;
|
|
render(<TabsHarness />);
|
|
|
|
expect(
|
|
screen.getByRole('tablist', { name: 'Space content' })
|
|
).toHaveAttribute('data-motion-reduced', 'true');
|
|
expect(
|
|
screen.getByRole('tablist', { name: 'Space content' })
|
|
).toHaveAttribute('data-layout-movement', 'instant');
|
|
});
|
|
});
|