Plan 2 of the web embedding SDK: a self-contained, framework-agnostic TypeScript core wrapping the Plan-1 wasm Viewer. createViewer -> OpViewer with load, read-only snapshots (document/pages/pageCount/activePage), pan/zoom navigation (+ wheel), SVG export, load/viewportchange events, and destroy. Read-only: no .op editing/mutation. Self-contained: zero @zseven-w/pen-* deps (types vendored from jian-ops-schema ts-rs), tsup build, vitest+jsdom tests that mock the wasm seam. 10/10 tests; tsc clean; post-destroy use-after-free guarded. React+Vue adapters follow in Plan 3.
37 lines
1.6 KiB
TypeScript
37 lines
1.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { MockViewer, mockExport } from './mock-wasm.js';
|
|
let current: MockViewer;
|
|
vi.mock('../src/wasm.js', () => ({
|
|
ensureWasm: vi.fn(async () => {}), WasmViewer: vi.fn(() => { current = new MockViewer(); return current; }), _export: mockExport,
|
|
}));
|
|
import { createViewer } from '../src/index.js';
|
|
|
|
describe('export + destroy', () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
it('exports svg bytes and rejects png', async () => {
|
|
const v = await createViewer({ canvas: document.createElement('canvas') });
|
|
expect(v.export({ format: 'svg' })).toBeInstanceOf(Uint8Array);
|
|
expect(() => v.export({ format: 'png' as 'svg' })).toThrow();
|
|
});
|
|
it('destroy detaches, frees, and removes the wheel listener', async () => {
|
|
const canvas = document.createElement('canvas');
|
|
const v = await createViewer({ canvas });
|
|
v.destroy();
|
|
expect(current.detach).toHaveBeenCalled();
|
|
expect(current.free).toHaveBeenCalled();
|
|
canvas.dispatchEvent(new WheelEvent('wheel', { bubbles: true }));
|
|
expect(current.forward_wheel).not.toHaveBeenCalled();
|
|
});
|
|
it('calling a method after destroy throws the use-after-free error', async () => {
|
|
const v = await createViewer({ canvas: document.createElement('canvas') });
|
|
v.destroy();
|
|
expect(() => v.setZoom(2)).toThrow('op-web-sdk: viewer has been destroyed');
|
|
});
|
|
it('double-destroy is a no-op and does not call free twice', async () => {
|
|
const v = await createViewer({ canvas: document.createElement('canvas') });
|
|
v.destroy();
|
|
expect(() => v.destroy()).not.toThrow();
|
|
expect(current.free).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|