feat: redesign Toolbox navigation and personalization

This commit is contained in:
2026-07-23 00:33:03 +02:00
parent 587aadb79b
commit 67ba5e3eca
25 changed files with 1075 additions and 427 deletions

View File

@@ -1,4 +1,10 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import App from './App';
@@ -16,14 +22,14 @@ describe('portal UI', () => {
await screen.findByRole('heading', { name: 'PDF Workbench' })
).toBeInTheDocument();
expect(
screen.getByText(
screen.getAllByText(
'Local processing · no file uploads declared · no telemetry declared'
)
).toBeInTheDocument();
).toHaveLength(3);
const sourceLinks = screen.getAllByRole('link', {
name: 'Source and license',
});
expect(sourceLinks).toHaveLength(2);
expect(sourceLinks).toHaveLength(4);
expect(
sourceLinks.find(
(link) =>
@@ -33,10 +39,15 @@ describe('portal UI', () => {
).toHaveAttribute('rel', 'noopener noreferrer');
expect(
sourceLinks.find((link) =>
link.getAttribute('href')?.endsWith('/toolbox-portal/src/tag/v0.1.0')
link.getAttribute('href')?.endsWith('/toolbox-portal/src/tag/v0.4.0')
)
).toBeDefined();
const launch = screen.getByRole('link', { name: /open tool/i });
expect(
screen.queryByRole('region', { name: 'Pinned' })
).not.toBeInTheDocument();
const launch = screen
.getAllByRole('link', { name: 'PDF Workbench' })
.find((link) => link.classList.contains('app-card__launch-link'))!;
expect(
new URL(launch.getAttribute('href')!).searchParams.get('toolbox')
).toContain('toolbox.catalog.json');
@@ -47,7 +58,15 @@ describe('portal UI', () => {
'de.add-ideas.pdf-tools'
)
);
expect(screen.getByText('Pinned')).toBeInTheDocument();
expect(screen.getByRole('region', { name: 'Pinned' })).toContainElement(
screen.getByTestId('app-card-de.add-ideas.pdf-tools')
);
expect(
screen.queryByRole('button', { name: /Move PDF Workbench/i })
).not.toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Reorder PDF Workbench' })
).toBeInTheDocument();
await user.click(
screen.getByRole('button', { name: 'Hide PDF Workbench' })
@@ -61,6 +80,144 @@ describe('portal UI', () => {
).toBeInTheDocument();
});
it('lists contextual destinations in the Apps menu', async () => {
vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
const user = userEvent.setup();
render(<App />);
await screen.findByRole('heading', { name: 'PDF Workbench' });
await user.click(screen.getByText('Apps').closest('summary')!);
const navigation = screen.getByRole('navigation', {
name: 'Toolbox applications',
});
const destination = within(navigation).getByRole('link', {
name: 'XSLT Workbench',
});
expect(
new URL(destination.getAttribute('href')!).searchParams.get('toolbox')
).toContain('toolbox.catalog.json');
});
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' }));
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' }));
fireEvent(
window,
new StorageEvent('storage', {
key: PREFERENCES_KEY,
newValue: JSON.stringify({
version: 1,
pinned: [],
order: [
'de.add-ideas.pdf-tools',
'de.add-ideas.xslt-tools',
'de.add-ideas.onenote-tools',
],
hidden: [],
theme: 'light',
}),
})
);
expect(document.documentElement).toHaveAttribute('data-theme', 'light');
});
it('supports keyboard drag ordering from each tile grip', async () => {
vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
render(<App />);
await screen.findByRole('heading', { name: 'PDF Workbench' });
const cards = [
screen.getByTestId('app-card-de.add-ideas.pdf-tools'),
screen.getByTestId('app-card-de.add-ideas.xslt-tools'),
screen.getByTestId('app-card-de.add-ideas.onenote-tools'),
];
cards.forEach((card, index) => {
const rect = {
x: index * 220,
y: 0,
width: 200,
height: 320,
top: 0,
right: index * 220 + 200,
bottom: 320,
left: index * 220,
toJSON: () => ({}),
} as DOMRect;
card.getBoundingClientRect = () => rect;
card.getClientRects = () => [rect] as unknown as DOMRectList;
});
const grip = screen.getByRole('button', { name: 'Reorder PDF Workbench' });
grip.focus();
fireEvent.keyDown(grip, { key: ' ', code: 'Space' });
await waitFor(() => expect(grip).toHaveAttribute('aria-pressed', 'true'));
await new Promise((resolve) => setTimeout(resolve, 0));
fireEvent.keyDown(document, { key: 'ArrowRight', code: 'ArrowRight' });
fireEvent.keyDown(document, { key: ' ', code: 'Space' });
await waitFor(() => {
const tools = screen.getByRole('region', { name: 'Tools' });
expect(
within(tools)
.getAllByRole('heading', { level: 3 })
.map((heading) => heading.textContent)
).toEqual(['XSLT Workbench', 'PDF Workbench', 'OneNote Reader']);
});
expect(
JSON.parse(localStorage.getItem(PREFERENCES_KEY) ?? '{}').order
).toEqual([
'de.add-ideas.xslt-tools',
'de.add-ideas.pdf-tools',
'de.add-ideas.onenote-tools',
]);
});
it('filters by clickable tags and clears an active tag', async () => {
vi.stubGlobal('fetch', vi.fn(catalogueFetch()));
const user = userEvent.setup();
render(<App />);
const merge = await screen.findByRole('button', { name: '#merge' });
await user.click(merge);
expect(merge).toHaveAttribute('aria-pressed', 'true');
expect(
screen.getByRole('heading', { name: 'PDF Workbench' })
).toBeInTheDocument();
expect(
screen.queryByRole('heading', { name: 'XSLT Workbench' })
).not.toBeInTheDocument();
expect(
screen.queryByRole('heading', { name: 'OneNote Reader' })
).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: '#merge' }));
expect(
await screen.findByRole('heading', { name: 'XSLT Workbench' })
).toBeInTheDocument();
expect(
screen.getByRole('heading', { name: 'OneNote Reader' })
).toBeInTheDocument();
});
it('opens the standard help control and explains tile interaction', 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: 'Help' }));
expect(
screen.getByRole('dialog', { name: 'Choose and arrange your tools' })
).toHaveTextContent('click anywhere on a tool tile');
await user.click(screen.getByRole('button', { name: 'Close help' }));
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('shows a recoverable catalogue error', async () => {
const fetchMock = vi
.fn()