feat: add shared Toolbox header and themes

This commit is contained in:
2026-07-23 00:29:21 +02:00
parent 8f8a59e76c
commit d5574a0a44
16 changed files with 451 additions and 176 deletions

View File

@@ -0,0 +1,48 @@
import { cleanup, render, waitFor } from '@testing-library/react';
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
it,
vi,
} from 'vitest';
import CodeMirrorEditor from '../src/editor/CodeMirrorEditor';
const originalGetClientRects = Range.prototype.getClientRects;
beforeAll(() => {
Range.prototype.getClientRects = () => [] as unknown as DOMRectList;
});
afterAll(() => {
Range.prototype.getClientRects = originalGetClientRects;
});
afterEach(cleanup);
describe('CodeMirror themes', () => {
it('reconfigures the editor when the Toolbox theme changes', async () => {
const onChange = vi.fn();
const { container, rerender } = render(
<div data-toolbox-theme="dark">
<CodeMirrorEditor value="<root/>" onChange={onChange} ariaLabel="XML" />
</div>
);
const editorHost = container.querySelector<HTMLElement>('.editor-host');
expect(editorHost?.dataset.editorTheme).toBe('dark');
expect(container.querySelector('.cm-editor')).toBeTruthy();
rerender(
<div data-toolbox-theme="light">
<CodeMirrorEditor value="<root/>" onChange={onChange} ariaLabel="XML" />
</div>
);
await waitFor(() => {
expect(editorHost?.dataset.editorTheme).toBe('light');
});
});
});

View File

@@ -70,6 +70,11 @@ describe('release packaging', () => {
)
)
).toBe(true);
expect(
names.some((name) =>
/^LICENSES\/npm\/codemirror--theme-one-dark-[^/]+\/LICENSE$/.test(name)
)
).toBe(true);
expect(names.some((name) => /\/react-[^/]+\/LICENSE$/.test(name))).toBe(
true
);

View File

@@ -1,5 +1,11 @@
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AppShell } from '@add-ideas/toolbox-shell-react';
import type {
ToolboxAppManifest,
@@ -7,6 +13,7 @@ import type {
} from '@add-ideas/toolbox-contract';
import { toolboxApp } from '../src/toolboxApp';
beforeEach(() => localStorage.clear());
afterEach(cleanup);
const pdfApp: ToolboxAppManifest = {
@@ -46,25 +53,51 @@ const catalog: ToolboxCatalog = {
};
describe('toolbox shell integration', () => {
it('keeps the app usable standalone with its XSLT Help action', () => {
it('shows the standard app controls when running standalone', () => {
const onHelp = vi.fn();
render(
<AppShell
app={toolboxApp}
contextOptions={{ location: 'https://example.test/deep/nested/app/' }}
appActions={<button type="button">Help</button>}
helpAction={{ onClick: onHelp, label: 'Help' }}
>
<p>XSLT workbench</p>
</AppShell>
);
expect(screen.getByText('XSLT workbench')).toBeTruthy();
expect(screen.getByRole('button', { name: 'Help' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Help' }));
expect(onHelp).toHaveBeenCalledOnce();
expect(
screen.getByRole('link', { name: 'Source' }).getAttribute('href')
).toBe('https://git.add-ideas.de/zemion/xslt-tools/src/tag/v0.3.2');
screen
.getByRole('link', { name: 'Source for XSLT tools on Gitea' })
.getAttribute('href')
).toBe('https://git.add-ideas.de/zemion/xslt-tools');
expect(screen.getByText('Personalize')).toBeTruthy();
expect(screen.getByText('Apps')).toBeTruthy();
});
it('persists explicit light, dark, and system theme choices', () => {
const { container } = render(
<AppShell app={toolboxApp}>
<p>XSLT workbench</p>
</AppShell>
);
const shell = container.firstElementChild;
expect(shell?.getAttribute('data-toolbox-theme')).toBe('system');
fireEvent.click(screen.getByText('Personalize').closest('summary')!);
fireEvent.click(screen.getByRole('button', { name: 'Dark' }));
expect(shell?.getAttribute('data-toolbox-theme')).toBe('dark');
fireEvent.click(screen.getByRole('button', { name: 'Light' }));
expect(shell?.getAttribute('data-toolbox-theme')).toBe('light');
fireEvent.click(screen.getByRole('button', { name: 'System' }));
expect(shell?.getAttribute('data-toolbox-theme')).toBe('system');
expect(
screen.queryByRole('navigation', { name: 'Toolbox applications' })
).toBeNull();
localStorage.getItem('@add-ideas/toolbox-portal:v1:preferences')
).toContain('"theme":"system"');
});
it('loads a same-origin catalogue and carries context through switcher links', async () => {
@@ -81,6 +114,7 @@ describe('toolbox shell integration', () => {
</AppShell>
);
fireEvent.click(screen.getByText('Apps').closest('summary')!);
const navigation = await screen.findByRole('navigation', {
name: 'Toolbox applications',
});