feat: open multi-part packages in browser

This commit is contained in:
2026-07-22 19:46:17 +02:00
parent 2c998952e5
commit 3b1777a5b3
11 changed files with 428 additions and 64 deletions

View File

@@ -52,10 +52,14 @@ class SuccessfulParserWorker implements WorkerPort {
onmessage: ((event: MessageEvent<WorkerResponse>) => void) | null = null;
onerror: ((event: ErrorEvent) => void) | null = null;
terminated = false;
lastMessage: WorkerRequest | undefined;
lastTransfer: Transferable[] = [];
constructor(private readonly packageResult?: OneNotePackageDto) {}
postMessage(message: WorkerRequest): void {
postMessage(message: WorkerRequest, transfer: Transferable[] = []): void {
this.lastMessage = message;
this.lastTransfer = transfer;
queueMicrotask(() => {
if (message.type === 'parse-one') {
this.respond({
@@ -158,7 +162,7 @@ describe('OneNoteApplication', () => {
);
await user.upload(
screen.getByLabelText('Choose a OneNote file'),
screen.getByLabelText('Choose OneNote files'),
new File([new Uint8Array([1, 2, 3])], 'Section.one')
);
@@ -189,23 +193,74 @@ describe('OneNoteApplication', () => {
tree: { interpreted: false, nodes: [] },
diagnostics: [],
};
const worker = new SuccessfulParserWorker(notebook);
render(
<OneNoteApplication
createWorkerClient={() =>
new OneNoteWorkerClient(() => new SuccessfulParserWorker(notebook))
}
createWorkerClient={() => 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: [new File([new Uint8Array([1])], 'Notebook.onepkg')],
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(
<OneNoteApplication
createWorkerClient={() => 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 () => {
@@ -299,7 +354,7 @@ describe('OneNoteApplication', () => {
);
await user.upload(
screen.getByLabelText('Choose a OneNote file'),
screen.getByLabelText('Choose OneNote files'),
new File([new Uint8Array([1])], 'Notebook.onepkg')
);
@@ -336,7 +391,7 @@ describe('OneNoteApplication', () => {
);
await user.upload(
screen.getByLabelText('Choose a OneNote file'),
screen.getByLabelText('Choose OneNote files'),
new File(['not OneNote'], 'Broken.one')
);
@@ -353,12 +408,12 @@ describe('OneNoteApplication', () => {
render(<OneNoteApplication />);
await user.upload(
screen.getByLabelText('Choose a OneNote file'),
screen.getByLabelText('Choose OneNote files'),
new File(['not a notebook'], 'notes.zip')
);
expect(await screen.findByRole('alert')).toHaveTextContent(
'Choose a file whose name ends in .one or .onepkg.'
'Choose one .one file, or one .onepkg file together with its .cab parts.'
);
});
@@ -371,7 +426,7 @@ describe('OneNoteApplication', () => {
value: MAX_SOURCE_FILE_BYTES + 1,
});
await user.upload(screen.getByLabelText('Choose a OneNote file'), file);
await user.upload(screen.getByLabelText('Choose OneNote files'), file);
expect(await screen.findByRole('alert')).toHaveTextContent('512 MiB');
});