fix: stabilize Toolbox navigation layout
This commit is contained in:
163
src/App.test.tsx
163
src/App.test.tsx
@@ -39,12 +39,17 @@ describe('portal UI', () => {
|
||||
).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
expect(
|
||||
sourceLinks.find((link) =>
|
||||
link.getAttribute('href')?.endsWith('/toolbox-portal/src/tag/v0.4.0')
|
||||
link.getAttribute('href')?.endsWith('/toolbox-portal/src/tag/v0.4.1')
|
||||
)
|
||||
).toBeDefined();
|
||||
expect(
|
||||
screen.queryByRole('region', { name: 'Pinned' })
|
||||
).not.toBeInTheDocument();
|
||||
const showHidden = screen.getByRole('checkbox', {
|
||||
name: 'Show hidden (0)',
|
||||
});
|
||||
expect(showHidden).toBeDisabled();
|
||||
expect(showHidden).not.toBeChecked();
|
||||
const launch = screen
|
||||
.getAllByRole('link', { name: 'PDF Workbench' })
|
||||
.find((link) => link.classList.contains('app-card__launch-link'))!;
|
||||
@@ -74,10 +79,74 @@ describe('portal UI', () => {
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: 'PDF Workbench' })
|
||||
).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /show hidden/i }));
|
||||
expect(showHidden).toBeEnabled();
|
||||
expect(showHidden).toHaveAccessibleName('Show hidden (1)');
|
||||
fireEvent.click(showHidden);
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'PDF Workbench' })
|
||||
).toBeInTheDocument();
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Show PDF Workbench' })
|
||||
);
|
||||
expect(showHidden).toBeDisabled();
|
||||
expect(showHidden).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('keeps search and category controls together and filters with each', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
|
||||
const user = userEvent.setup();
|
||||
render(<App />);
|
||||
await screen.findByRole('heading', { name: 'PDF Workbench' });
|
||||
|
||||
const search = screen.getByRole('searchbox', { name: 'Search tools' });
|
||||
const category = screen.getByRole('combobox', {
|
||||
name: 'Filter by category',
|
||||
});
|
||||
expect(search.closest('.tool-controls')).toBe(
|
||||
category.closest('.tool-controls')
|
||||
);
|
||||
|
||||
await user.type(search, 'xslt');
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'XSLT Workbench' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: 'PDF Workbench' })
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await user.clear(search);
|
||||
await user.selectOptions(category, 'notes');
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'OneNote Reader' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: 'XSLT Workbench' })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears search filters without revealing hidden tools', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
|
||||
const user = userEvent.setup();
|
||||
render(<App />);
|
||||
await screen.findByRole('heading', { name: 'PDF Workbench' });
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Hide PDF Workbench' })
|
||||
);
|
||||
await user.type(
|
||||
screen.getByRole('searchbox', { name: 'Search tools' }),
|
||||
'no tool has this phrase'
|
||||
);
|
||||
await user.click(screen.getByRole('button', { name: 'Clear filters' }));
|
||||
|
||||
expect(
|
||||
screen.queryByRole('heading', { name: 'PDF Workbench' })
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'XSLT Workbench' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('checkbox', { name: 'Show hidden (1)' })
|
||||
).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('lists contextual destinations in the Apps menu', async () => {
|
||||
@@ -85,7 +154,7 @@ describe('portal UI', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<App />);
|
||||
await screen.findByRole('heading', { name: 'PDF Workbench' });
|
||||
await user.click(screen.getByText('Apps').closest('summary')!);
|
||||
await user.click(screen.getByRole('button', { name: 'Apps' }));
|
||||
const navigation = screen.getByRole('navigation', {
|
||||
name: 'Toolbox applications',
|
||||
});
|
||||
@@ -97,18 +166,70 @@ describe('portal UI', () => {
|
||||
).toContain('toolbox.catalog.json');
|
||||
});
|
||||
|
||||
it('uses the shared accessible header identity and control order', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
|
||||
render(<App />);
|
||||
await screen.findByRole('heading', { name: 'PDF Workbench' });
|
||||
|
||||
const header = document.querySelector<HTMLElement>('.portal-header')!;
|
||||
expect(
|
||||
within(header).getByRole('heading', { level: 1, name: 'Toolbox' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(header).getByText('Local-first browser tools')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
level: 2,
|
||||
name: 'Your document tools, in one calm place.',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getAllByRole('heading', {
|
||||
name: 'Your document tools, in one calm place.',
|
||||
})
|
||||
).toHaveLength(1);
|
||||
|
||||
const personalize = within(header).getByRole('button', {
|
||||
name: 'Personalize',
|
||||
});
|
||||
const apps = within(header).getByRole('button', { name: 'Apps' });
|
||||
const source = within(header).getByRole('link', {
|
||||
name: 'Toolbox source on Gitea',
|
||||
});
|
||||
const help = within(header).getByRole('button', { name: 'Help' });
|
||||
const brand = within(header).getByRole('link', {
|
||||
name: 'add·ideas Toolbox',
|
||||
});
|
||||
expect(
|
||||
personalize.compareDocumentPosition(apps) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
apps.compareDocumentPosition(source) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
source.compareDocumentPosition(help) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||
).toBeTruthy();
|
||||
expect(brand.querySelector('img')).toHaveAttribute('src', './favicon.svg');
|
||||
});
|
||||
|
||||
it('persists an explicit appearance and accepts cross-tab updates', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
|
||||
const user = userEvent.setup();
|
||||
render(<App />);
|
||||
await screen.findByRole('heading', { name: 'PDF Workbench' });
|
||||
await user.click(screen.getByRole('button', { name: 'Personalize' }));
|
||||
const personalize = screen.getByRole('button', { name: 'Personalize' });
|
||||
await user.click(personalize);
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Personalize your toolbox' })
|
||||
).toBeInTheDocument();
|
||||
await user.click(screen.getByRole('button', { name: 'Dark' }));
|
||||
expect(document.documentElement).toHaveAttribute('data-theme', 'dark');
|
||||
await waitFor(() =>
|
||||
expect(localStorage.getItem(PREFERENCES_KEY)).toContain('"theme":"dark"')
|
||||
);
|
||||
await user.click(screen.getByRole('button', { name: 'Close preferences' }));
|
||||
await user.click(personalize);
|
||||
|
||||
fireEvent(
|
||||
window,
|
||||
@@ -208,14 +329,16 @@ describe('portal UI', () => {
|
||||
it('opens the standard help control and explains tile interaction', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
|
||||
const user = userEvent.setup();
|
||||
render(<App />);
|
||||
const { container } = render(<App />);
|
||||
await screen.findByRole('heading', { name: 'PDF Workbench' });
|
||||
await user.click(screen.getByRole('button', { name: 'Help' }));
|
||||
expect(
|
||||
screen.getByRole('dialog', { name: 'Choose and arrange your tools' })
|
||||
).toHaveTextContent('click anywhere on a tool tile');
|
||||
expect(container.querySelector('.site-shell')).toHaveAttribute('inert');
|
||||
await user.click(screen.getByRole('button', { name: 'Close help' }));
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(container.querySelector('.site-shell')).not.toHaveAttribute('inert');
|
||||
});
|
||||
|
||||
it('shows a recoverable catalogue error', async () => {
|
||||
@@ -249,31 +372,35 @@ describe('portal UI', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('contains preference focus and restores it to Personalize', async () => {
|
||||
it('hosts compact preferences in the shared Personalize popover', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<App />);
|
||||
const personalize = screen.getByRole('button', { name: 'Personalize' });
|
||||
|
||||
await user.click(personalize);
|
||||
const dialog = screen.getByRole('dialog', {
|
||||
const heading = screen.getByRole('heading', {
|
||||
name: 'Personalize your toolbox',
|
||||
});
|
||||
const close = screen.getByRole('button', { name: 'Close preferences' });
|
||||
expect(close).toHaveFocus();
|
||||
expect(container.querySelector('.site-shell')).toHaveAttribute('inert');
|
||||
expect(dialog).toBeInTheDocument();
|
||||
const popover = screen.getByRole('dialog', { name: 'Personalize Toolbox' });
|
||||
expect(popover).toContainElement(heading);
|
||||
expect(popover).not.toHaveAttribute('aria-modal');
|
||||
expect(container.querySelector('.site-shell')).not.toHaveAttribute('inert');
|
||||
expect(screen.getByText(/does not transmit them/i)).toHaveTextContent(
|
||||
/Trusted code in another Toolbox app on this site can access same-origin storage/i
|
||||
);
|
||||
|
||||
const reset = screen.getByRole('button', { name: 'Reset' });
|
||||
reset.focus();
|
||||
await user.tab();
|
||||
expect(close).toHaveFocus();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Export JSON' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Import JSON' })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Reset' })).toBeInTheDocument();
|
||||
|
||||
await user.keyboard('{Escape}');
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('dialog', { name: 'Personalize Toolbox' })
|
||||
).not.toBeInTheDocument();
|
||||
expect(personalize).toHaveFocus();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user