Files
onenote-tools/src/App.test.tsx

379 lines
11 KiB
TypeScript

import { fireEvent, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';
import { OneNoteApplication } from './App.js';
import type {
OneNotePackageDto,
OneNoteSectionDto,
} from './onenote/model/dto.js';
import {
MAX_SOURCE_FILE_BYTES,
sourceKindFromFileName,
} from './source-file.js';
import {
OneNoteWorkerClient,
type WorkerPort,
} from './worker/onenote.client.js';
import type {
WorkerRequest,
WorkerResponse,
} from './worker/worker-protocol.js';
const section: OneNoteSectionDto = {
format: 'one',
sourceName: 'Section.one',
sectionName: 'Project notes',
pages: [
{
id: 'page-1',
title: 'First meeting',
text: 'This is the complete extracted page text.',
textPreview: 'This is the complete extracted page text.',
createdAt: '2024-01-02T10:00:00.000Z',
},
],
diagnostics: [
{
severity: 'warning',
code: 'UNSUPPORTED_IMAGE',
message: 'An image was not rendered.',
recoverable: true,
},
],
parserInfo: {
implementation: 'typescript',
supportedVariant: 'desktop-revision-store',
referenceRevision: 'test',
},
};
class SuccessfulParserWorker implements WorkerPort {
onmessage: ((event: MessageEvent<WorkerResponse>) => void) | null = null;
onerror: ((event: ErrorEvent) => void) | null = null;
terminated = false;
constructor(private readonly packageResult?: OneNotePackageDto) {}
postMessage(message: WorkerRequest): void {
queueMicrotask(() => {
if (message.type === 'parse-one') {
this.respond({
type: 'section',
requestId: message.requestId,
sessionId: 'session-one',
section,
});
} else if (message.type === 'parse-onepkg' && this.packageResult) {
this.respond({
type: 'package',
requestId: message.requestId,
sessionId: 'session-package',
notebook: this.packageResult,
});
} else if (message.type === 'get-page') {
const page = section.pages[0]!;
this.respond({
type: 'page',
requestId: message.requestId,
sessionId: message.sessionId,
page: {
...page,
id: message.pageId,
blocks: [
{
kind: 'text',
id: 'text-1',
sourceText: page.text,
text: page.text,
runs: [
{
start: 0,
end: page.text.length,
text: page.text,
style: {},
},
],
paragraphStyle: {},
layout: {},
},
],
diagnostics: [],
},
});
}
});
}
terminate(): void {
this.terminated = true;
}
private respond(response: WorkerResponse): void {
this.onmessage?.(new MessageEvent('message', { data: response }));
}
}
class FailingParserWorker implements WorkerPort {
onmessage: ((event: MessageEvent<WorkerResponse>) => void) | null = null;
onerror: ((event: ErrorEvent) => void) | null = null;
postMessage(message: WorkerRequest): void {
if (message.type !== 'parse-one' && message.type !== 'parse-onepkg') return;
queueMicrotask(() => {
this.onmessage?.(
new MessageEvent('message', {
data: {
type: 'failure',
requestId: message.requestId,
error: {
code: 'invalid-format',
message: 'The source header is not a recognized OneNote file.',
recoverable: false,
structure: 'OneStore header',
},
} satisfies WorkerResponse,
})
);
});
}
terminate(): void {}
}
describe('OneNoteApplication', () => {
it('recognizes only supported local filename extensions', () => {
expect(sourceKindFromFileName('Section.ONE')).toBe('one');
expect(sourceKindFromFileName('Notebook.OnePkg')).toBe('onepkg');
expect(sourceKindFromFileName('notes.zip')).toBeNull();
});
it('opens a section, navigates its page, shows diagnostics, and clears', async () => {
const user = userEvent.setup();
const worker = new SuccessfulParserWorker();
render(
<OneNoteApplication
createWorkerClient={() => new OneNoteWorkerClient(() => worker)}
/>
);
await user.upload(
screen.getByLabelText('Choose a OneNote file'),
new File([new Uint8Array([1, 2, 3])], 'Section.one')
);
expect(
await screen.findByRole('heading', { name: 'First meeting' })
).toBeInTheDocument();
expect(
within(screen.getByRole('article', { name: 'First meeting' })).getByText(
'This is the complete extracted page text.'
)
).toBeInTheDocument();
expect(screen.getByText('UNSUPPORTED_IMAGE')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Clear file' }));
expect(worker.terminated).toBe(true);
expect(
screen.getByRole('heading', { name: 'Open a OneNote file' })
).toBeInTheDocument();
});
it('opens a dropped OneNote package through the local worker', async () => {
const notebook: OneNotePackageDto = {
format: 'onepkg',
sourceName: 'Notebook.onepkg',
sourceSize: 1,
entries: [],
sections: [],
tree: { interpreted: false, nodes: [] },
diagnostics: [],
};
render(
<OneNoteApplication
createWorkerClient={() =>
new OneNoteWorkerClient(() => new SuccessfulParserWorker(notebook))
}
/>
);
fireEvent.drop(screen.getByTestId('file-drop-zone'), {
dataTransfer: {
files: [new File([new Uint8Array([1])], 'Notebook.onepkg')],
},
});
expect(
await screen.findByRole('heading', { name: 'Package summary' })
).toBeInTheDocument();
});
it('keeps a failed packaged section visible beside a readable one', async () => {
const notebook: OneNotePackageDto = {
format: 'onepkg',
sourceName: 'Notebook.onepkg',
sourceSize: 4096,
entries: [
{
path: 'Notes/Readable.one',
normalizedPath: 'Notes/Readable.one',
kind: 'section',
uncompressedSize: 2048,
compressionMethod: 'LZX',
parseStatus: 'parsed',
},
{
path: 'Notes/Broken.one',
normalizedPath: 'Notes/Broken.one',
kind: 'section',
uncompressedSize: 1024,
compressionMethod: 'LZX',
parseStatus: 'failed',
},
],
sections: [
{
id: 'readable',
path: 'Notes/Readable.one',
displayName: 'Readable',
parseStatus: 'parsed',
section,
diagnostics: [],
},
{
id: 'broken',
path: 'Notes/Broken.one',
displayName: 'Broken',
parseStatus: 'failed',
diagnostics: [
{
severity: 'error',
code: 'SECTION_PARSE_FAILED',
message: 'This section is malformed.',
recoverable: true,
},
],
},
],
tree: {
interpreted: true,
tocPath: 'Open Notebook.onetoc2',
nodes: [
{
kind: 'section-group',
id: 'Notes',
displayName: 'Notes',
children: [
{
kind: 'section',
id: 'readable',
sectionId: 'readable',
displayName: 'Readable',
},
{
kind: 'section',
id: 'broken',
sectionId: 'broken',
displayName: 'Broken',
},
],
},
],
},
diagnostics: [
{
severity: 'warning',
code: 'TEST_PACKAGE_WARNING',
message: 'Example recoverable package warning.',
recoverable: true,
},
],
};
const user = userEvent.setup();
render(
<OneNoteApplication
createWorkerClient={() =>
new OneNoteWorkerClient(() => new SuccessfulParserWorker(notebook))
}
/>
);
await user.upload(
screen.getByLabelText('Choose a OneNote file'),
new File([new Uint8Array([1])], 'Notebook.onepkg')
);
expect(
await screen.findByRole('heading', { name: 'Package summary' })
).toBeInTheDocument();
expect(screen.getByText('Notes')).toBeVisible();
expect(screen.getByText('OneNote notebook order')).toBeVisible();
expect(
screen.getByRole('button', { name: /Broken, failed/i })
).toBeVisible();
expect(screen.getByText('TEST_PACKAGE_WARNING')).toBeInTheDocument();
expect(
await within(
screen.getByRole('article', { name: 'First meeting' })
).findByText('This is the complete extracted page text.')
).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /Broken, failed/i }));
expect(screen.getByText('SECTION_PARSE_FAILED')).toBeInTheDocument();
expect(
screen.getByText('This section did not produce a readable page list.')
).toBeInTheDocument();
});
it('shows a controlled parser failure', async () => {
const user = userEvent.setup();
render(
<OneNoteApplication
createWorkerClient={() =>
new OneNoteWorkerClient(() => new FailingParserWorker())
}
/>
);
await user.upload(
screen.getByLabelText('Choose a OneNote file'),
new File(['not OneNote'], 'Broken.one')
);
expect(await screen.findByRole('alert')).toHaveTextContent(
'The source header is not a recognized OneNote file.'
);
expect(screen.getByRole('alert')).toHaveTextContent(
'invalid-format · OneStore header'
);
});
it('rejects unrelated extensions without starting a worker', async () => {
const user = userEvent.setup({ applyAccept: false });
render(<OneNoteApplication />);
await user.upload(
screen.getByLabelText('Choose a OneNote file'),
new File(['not a notebook'], 'notes.zip')
);
expect(await screen.findByRole('alert')).toHaveTextContent(
'Choose a file whose name ends in .one or .onepkg.'
);
});
it('enforces the source-size safety limit before reading', async () => {
const user = userEvent.setup();
render(<OneNoteApplication />);
const file = new File(['tiny'], 'large.one');
Object.defineProperty(file, 'size', {
configurable: true,
value: MAX_SOURCE_FILE_BYTES + 1,
});
await user.upload(screen.getByLabelText('Choose a OneNote file'), file);
expect(await screen.findByRole('alert')).toHaveTextContent('512 MiB');
});
});