mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
244 lines
8.2 KiB
TypeScript
244 lines
8.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
openWorkspaceTab,
|
|
WorkspaceTabsBar,
|
|
} from '../../src/components/WorkspaceTabsBar';
|
|
import { navigate, type Route } from '../../src/router';
|
|
import type { Project } from '../../src/types';
|
|
|
|
vi.mock('../../src/i18n', () => ({
|
|
useI18n: () => ({
|
|
locale: 'en',
|
|
setLocale: () => undefined,
|
|
t: (key: string) => key,
|
|
}),
|
|
useT: () => (key: string) => {
|
|
const labels: Record<string, string> = {
|
|
'app.brand': 'Open Design',
|
|
'common.close': 'Close',
|
|
'common.untitled': 'Untitled',
|
|
'entry.navDesignSystems': 'Design systems',
|
|
'entry.navHome': 'Home',
|
|
'entry.navProjects': 'Projects',
|
|
'entry.navAutomations': 'Automations',
|
|
'entry.navPlugins': 'Plugins',
|
|
'entry.navIntegrations': 'Integrations',
|
|
'entry.metaProject': 'Project',
|
|
'entry.metaPlugins': 'Plugins',
|
|
'entry.metaWorkspace': 'Workspace',
|
|
'entry.metaStartProject': 'Start a new project',
|
|
'entry.noTabsFound': 'No tabs found',
|
|
'entry.pluginDetailsTitle': 'Plugin details',
|
|
'entry.marketplaceTitle': 'Marketplace',
|
|
'entry.workspaceTabsAria': 'Workspace tabs',
|
|
'entry.openWorkspacesAria': 'Open workspaces',
|
|
'entry.showHiddenTabs': 'Show hidden tabs',
|
|
'entry.moreTabs': '{count} more',
|
|
'entry.newTab': 'New tab',
|
|
'entry.searchTabs': 'Search tabs',
|
|
'entry.openTabs': 'Open tabs',
|
|
};
|
|
return labels[key] ?? key;
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../src/router', async () => {
|
|
const actual = await vi.importActual<typeof import('../../src/router')>(
|
|
'../../src/router',
|
|
);
|
|
return {
|
|
...actual,
|
|
navigate: vi.fn(),
|
|
};
|
|
});
|
|
|
|
const homeRoute: Route = { kind: 'home', view: 'home' };
|
|
const projectRoute: Route = {
|
|
kind: 'project',
|
|
projectId: 'project-alpha',
|
|
conversationId: null,
|
|
fileName: null,
|
|
};
|
|
|
|
const project: Project = {
|
|
id: 'project-alpha',
|
|
name: 'Project Alpha',
|
|
skillId: null,
|
|
designSystemId: null,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
};
|
|
|
|
describe('WorkspaceTabsBar navigation semantics', () => {
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
document.querySelector('[data-testid="blank-workspace-area"]')?.remove();
|
|
});
|
|
|
|
it('keeps Home tab as a singleton and avoids duplication', async () => {
|
|
const { rerender } = render(
|
|
<WorkspaceTabsBar route={{ kind: 'home', view: 'home' }} projects={[project]} />,
|
|
);
|
|
|
|
expect(screen.getAllByRole('tab')).toHaveLength(1);
|
|
|
|
// Clicking 'New tab' when a Home tab already exists should activate the existing Home tab
|
|
fireEvent.click(screen.getByRole('button', { name: 'New tab' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'New tab' }));
|
|
|
|
await waitFor(() => {
|
|
const labels = screen.getAllByRole('tab').map((tab) => tab.textContent ?? '');
|
|
expect(labels.filter((label) => label.includes('Home'))).toHaveLength(1);
|
|
});
|
|
|
|
// Navigate to projectRoute using rerender with a fresh object reference
|
|
rerender(<WorkspaceTabsBar route={{ ...projectRoute }} projects={[project]} />);
|
|
|
|
await waitFor(() => {
|
|
const labels = screen.getAllByRole('tab').map((tab) => tab.textContent ?? '');
|
|
expect(labels).toHaveLength(2);
|
|
expect(labels.some((label) => label.includes('Home'))).toBe(true);
|
|
expect(labels.some((label) => label.includes('Project Alpha'))).toBe(true);
|
|
});
|
|
|
|
// Return to Home by navigating back with a fresh route object reference
|
|
rerender(<WorkspaceTabsBar route={{ kind: 'home', view: 'home' }} projects={[project]} />);
|
|
|
|
await waitFor(() => {
|
|
const tabs = screen.getAllByRole('tab');
|
|
const labels = tabs.map((tab) => tab.textContent ?? '');
|
|
// Expect that we still have 2 tabs (Home and Project Alpha)
|
|
expect(tabs).toHaveLength(2);
|
|
expect(labels.filter((label) => label.includes('Home'))).toHaveLength(1);
|
|
expect(labels.filter((label) => label.includes('Project Alpha'))).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
it('can append and focus a project tab for create-project flows', async () => {
|
|
render(<WorkspaceTabsBar route={{ kind: 'home', view: 'home' }} projects={[project]} />);
|
|
|
|
openWorkspaceTab({ ...projectRoute });
|
|
|
|
await waitFor(() => {
|
|
const labels = screen.getAllByRole('tab').map((tab) => tab.textContent ?? '');
|
|
expect(labels).toHaveLength(2);
|
|
expect(labels.some((label) => label.includes('Home'))).toBe(true);
|
|
expect(labels.some((label) => label.includes('Project Alpha'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('appends and activates a new Home tab when Home is closed and user navigates back to Home', async () => {
|
|
window.localStorage.setItem(
|
|
'open-design:workspace-tabs:v1',
|
|
JSON.stringify({
|
|
activeTabId: 'project:project-alpha',
|
|
tabs: [
|
|
{
|
|
id: 'project:project-alpha',
|
|
kind: 'project',
|
|
projectId: 'project-alpha',
|
|
createdAt: 1,
|
|
lastActiveAt: 1,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
|
|
const { rerender } = render(
|
|
<WorkspaceTabsBar route={{ ...projectRoute }} projects={[project]} />,
|
|
);
|
|
|
|
await waitFor(() => {
|
|
const labels = screen.getAllByRole('tab').map((tab) => tab.textContent ?? '');
|
|
expect(labels).toHaveLength(1);
|
|
expect(labels[0]).toContain('Project Alpha');
|
|
});
|
|
|
|
// Navigate to Home
|
|
rerender(<WorkspaceTabsBar route={{ kind: 'home', view: 'home' }} projects={[project]} />);
|
|
|
|
await waitFor(() => {
|
|
const labels = screen.getAllByRole('tab').map((tab) => tab.textContent ?? '');
|
|
// It should append a new Home tab, resulting in 2 tabs total (Project Alpha and Home)
|
|
expect(labels).toHaveLength(2);
|
|
expect(labels.filter((label) => label.includes('Home'))).toHaveLength(1);
|
|
expect(labels.filter((label) => label.includes('Project Alpha'))).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
it('deduplicates and cleans up restored Home tabs from old sessions', async () => {
|
|
window.localStorage.setItem(
|
|
'open-design:workspace-tabs:v1',
|
|
JSON.stringify({
|
|
activeTabId: 'entry:home:old-two',
|
|
tabs: [
|
|
{
|
|
id: 'entry:home:old-one',
|
|
kind: 'entry',
|
|
view: 'home',
|
|
createdAt: 1,
|
|
lastActiveAt: 1,
|
|
},
|
|
{
|
|
id: 'entry:home:old-two',
|
|
kind: 'entry',
|
|
view: 'home',
|
|
createdAt: 2,
|
|
lastActiveAt: 2,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
|
|
render(<WorkspaceTabsBar route={{ kind: 'home', view: 'home' }} projects={[project]} />);
|
|
|
|
await waitFor(() => {
|
|
const labels = screen.getAllByRole('tab').map((tab) => tab.textContent ?? '');
|
|
// Expect that the duplicate Home tabs are deduplicated to exactly one Home tab
|
|
expect(labels.filter((label) => label.includes('Home'))).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
it('creates a replacement Home tab when the last tab is closed', async () => {
|
|
render(<WorkspaceTabsBar route={{ kind: 'home', view: 'home' }} projects={[project]} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
|
|
|
await waitFor(() => {
|
|
const labels = screen.getAllByRole('tab').map((tab) => tab.textContent ?? '');
|
|
expect(labels).toHaveLength(1);
|
|
expect(labels[0]).toContain('Home');
|
|
});
|
|
expect(navigate).toHaveBeenCalledWith(homeRoute);
|
|
});
|
|
|
|
it('dismisses tab search when a blank page area handles the mouse down', async () => {
|
|
const outsideArea = document.createElement('div');
|
|
outsideArea.setAttribute('data-testid', 'blank-workspace-area');
|
|
outsideArea.addEventListener('mousedown', (event) => event.stopPropagation());
|
|
document.body.append(outsideArea);
|
|
|
|
render(<WorkspaceTabsBar route={{ kind: 'home', view: 'home' }} projects={[project]} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Search tabs' }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('dialog', { name: 'Search tabs' })).toBeTruthy();
|
|
});
|
|
|
|
fireEvent.mouseDown(outsideArea);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByRole('dialog', { name: 'Search tabs' })).toBeNull();
|
|
});
|
|
});
|
|
});
|