feat: redesign Toolbox navigation and personalization
This commit is contained in:
171
src/App.test.tsx
171
src/App.test.tsx
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user