import { fireEvent, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } 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) => void) | null = null; onerror: ((event: ErrorEvent) => void) | null = null; terminated = false; lastMessage: WorkerRequest | undefined; readonly messages: WorkerRequest[] = []; lastTransfer: Transferable[] = []; constructor( private readonly packageResult?: OneNotePackageDto, private readonly resourcePage = false ) {} postMessage(message: WorkerRequest, transfer: Transferable[] = []): void { this.lastMessage = message; this.messages.push(message); this.lastTransfer = transfer; 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: this.resourcePage ? [ { kind: 'image', id: 'image-1', resourceId: 'resource-image', filename: 'large.png', isBackground: false, layout: {}, }, ] : [ { 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: [], }, }); } else if (message.type === 'get-resource') { if (message.intent === 'preview') { this.respond({ type: 'failure', requestId: message.requestId, error: { code: 'resource-not-available', message: 'This image can be downloaded but is not approved for automatic preview.', recoverable: true, }, }); } else { this.respond({ type: 'resource', requestId: message.requestId, sessionId: message.sessionId, resource: { id: message.resourceId, kind: 'image', filename: 'large.png', extension: 'png', mediaType: 'image/png', browserRenderable: false, size: 4, bytes: Uint8Array.of(1, 2, 3, 4).buffer, }, }); } } else if (message.type === 'export-session') { this.respond({ type: 'export-artifact', requestId: message.requestId, sessionId: message.sessionId, artifact: { filename: 'Project notes.zip', mediaType: 'application/zip', bytes: Uint8Array.of(0x50, 0x4b, 3, 4).buffer, }, }); } }); } terminate(): void { this.terminated = true; } private respond(response: WorkerResponse): void { this.onmessage?.(new MessageEvent('message', { data: response })); } } class FailingParserWorker implements WorkerPort { onmessage: ((event: MessageEvent) => 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( new OneNoteWorkerClient(() => worker)} /> ); await user.upload( screen.getByLabelText('Choose OneNote files'), 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('downloads a worker-built export after explicit user action', async () => { const user = userEvent.setup(); const worker = new SuccessfulParserWorker(); const createObjectUrl = vi .spyOn(URL, 'createObjectURL') .mockReturnValue('blob:local-export'); const click = vi .spyOn(HTMLAnchorElement.prototype, 'click') .mockImplementation(() => undefined); render( new OneNoteWorkerClient(() => worker)} /> ); await user.upload( screen.getByLabelText('Choose OneNote files'), new File([new Uint8Array([1, 2, 3])], 'Section.one') ); await screen.findByRole('heading', { name: 'First meeting' }); await user.click(screen.getByRole('button', { name: 'Create export' })); expect( await screen.findByText('The export download is ready.') ).toBeVisible(); expect(worker.lastMessage).toEqual({ type: 'export-session', requestId: 'export-3', sessionId: 'session-one', format: 'static-zip', }); expect(createObjectUrl).toHaveBeenCalledWith( expect.objectContaining({ type: 'application/zip' }) ); expect(click).toHaveBeenCalledOnce(); }); it('requests a rejected preview separately from an explicit image download', async () => { const user = userEvent.setup(); const worker = new SuccessfulParserWorker(undefined, true); vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:local-image'); const click = vi .spyOn(HTMLAnchorElement.prototype, 'click') .mockImplementation(() => undefined); render( new OneNoteWorkerClient(() => worker)} /> ); await user.upload( screen.getByLabelText('Choose OneNote files'), new File([new Uint8Array([1, 2, 3])], 'Section.one') ); expect( await screen.findByText(/not approved for automatic preview/i) ).toBeVisible(); expect(worker.messages).toContainEqual( expect.objectContaining({ type: 'get-resource', resourceId: 'resource-image', intent: 'preview', }) ); expect( worker.messages.some( (message) => message.type === 'get-resource' && message.intent === 'download' ) ).toBe(false); await user.click( screen.getByRole('button', { name: 'Download original image' }) ); expect(worker.messages).toContainEqual( expect.objectContaining({ type: 'get-resource', resourceId: 'resource-image', intent: 'download', }) ); expect(click).toHaveBeenCalledOnce(); }); 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: [], }; const worker = new SuccessfulParserWorker(notebook); render( new OneNoteWorkerClient(() => worker)} /> ); const part3 = new File([new Uint8Array([3])], 'Notebook-3.cab'); const primary = new File([new Uint8Array([1])], 'Notebook.onepkg'); const part2 = new File([new Uint8Array([2])], 'Notebook-2.cab'); fireEvent.drop(screen.getByTestId('file-drop-zone'), { dataTransfer: { files: [part3, primary, part2], }, }); expect( await screen.findByRole('heading', { name: 'Package summary' }) ).toBeInTheDocument(); expect(worker.lastMessage).toEqual( expect.objectContaining({ type: 'parse-onepkg', fileName: 'Notebook.onepkg', cabinetParts: [ expect.objectContaining({ fileName: 'Notebook-3.cab' }), expect.objectContaining({ fileName: 'Notebook-2.cab' }), ], }) ); expect(worker.lastTransfer).toHaveLength(3); }); it('hands a multi-file picker selection to the package worker', async () => { const notebook: OneNotePackageDto = { format: 'onepkg', sourceName: 'Notebook.onepkg', sourceSize: 2, entries: [], sections: [], tree: { interpreted: false, nodes: [] }, diagnostics: [], }; const worker = new SuccessfulParserWorker(notebook); const user = userEvent.setup(); render( new OneNoteWorkerClient(() => worker)} /> ); const input = screen.getByLabelText('Choose OneNote files'); expect(input).toHaveAttribute('multiple'); expect(input).toHaveAttribute('accept', '.one,.onepkg,.cab'); await user.upload(input, [ new File([new Uint8Array([2])], 'Notebook-2.cab'), new File([new Uint8Array([1])], 'Notebook.onepkg'), ]); expect( await screen.findByRole('heading', { name: 'Package summary' }) ).toBeInTheDocument(); expect(worker.lastMessage).toEqual( expect.objectContaining({ type: 'parse-onepkg', fileName: 'Notebook.onepkg', cabinetParts: [expect.objectContaining({ fileName: 'Notebook-2.cab' })], }) ); }); 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( new OneNoteWorkerClient(() => new SuccessfulParserWorker(notebook)) } /> ); await user.upload( screen.getByLabelText('Choose OneNote files'), 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( new OneNoteWorkerClient(() => new FailingParserWorker()) } /> ); await user.upload( screen.getByLabelText('Choose OneNote files'), 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(); await user.upload( screen.getByLabelText('Choose OneNote files'), new File(['not a notebook'], 'notes.zip') ); expect(await screen.findByRole('alert')).toHaveTextContent( 'Choose one .one file, or one .onepkg file together with its .cab parts.' ); }); it('enforces the source-size safety limit before reading', async () => { const user = userEvent.setup(); render(); 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 OneNote files'), file); expect(await screen.findByRole('alert')).toHaveTextContent('512 MiB'); }); });