<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Native/WASM Mermaid and Typst SVG preview rendering on desktop and mobile, plus cross-platform Preview plugin integrations. * **Improvements** * Centralized, sanitized rendering bridge with automatic Typst font-directory handling and configurable native renderer selection. * More consistent and robust error serialization and worker-backed preview flows for improved stability and performance. * **Tests** * Extensive unit and integration tests for preview rendering, font discovery, sanitization, and error serialization. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
262 lines
6.3 KiB
TypeScript
262 lines
6.3 KiB
TypeScript
import { afterEach } from 'node:test';
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { OpClient } from '../client';
|
|
import { type MessageHandlers, transfer } from '../message';
|
|
import type { OpSchema } from '../types';
|
|
|
|
interface TestOps extends OpSchema {
|
|
add: [{ a: number; b: number }, number];
|
|
bin: [Uint8Array, Uint8Array];
|
|
sub: [Uint8Array, number];
|
|
init: [{ fastText?: boolean } | undefined, { ok: true }];
|
|
}
|
|
|
|
declare module 'vitest' {
|
|
interface TestContext {
|
|
producer: OpClient<TestOps>;
|
|
handlers: MessageHandlers;
|
|
postMessage: ReturnType<typeof vi.fn>;
|
|
}
|
|
}
|
|
|
|
describe('op client', () => {
|
|
beforeEach(ctx => {
|
|
const { port1 } = new MessageChannel();
|
|
// @ts-expect-error patch postMessage
|
|
port1.postMessage = vi.fn(port1.postMessage);
|
|
// @ts-expect-error patch postMessage
|
|
ctx.postMessage = port1.postMessage;
|
|
ctx.producer = new OpClient(port1, {
|
|
timeout: 1000,
|
|
});
|
|
// @ts-expect-error internal api
|
|
ctx.handlers = ctx.producer.handlers;
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should send call op', async ctx => {
|
|
// @ts-expect-error internal api
|
|
const pendingCalls = ctx.producer.pendingCalls;
|
|
const result = ctx.producer.call('add', { a: 1, b: 2 });
|
|
|
|
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
|
{
|
|
"id": "add:1",
|
|
"name": "add",
|
|
"payload": {
|
|
"a": 1,
|
|
"b": 2,
|
|
},
|
|
"type": "call",
|
|
}
|
|
`);
|
|
expect(pendingCalls.has('add:1')).toBe(true);
|
|
|
|
// fake consumer return
|
|
ctx.handlers.return({ type: 'return', id: 'add:1', data: 3 });
|
|
|
|
await expect(result).resolves.toBe(3);
|
|
|
|
expect(pendingCalls.has('add:1')).toBe(false);
|
|
});
|
|
|
|
it('should transfer transferables with call op', async ctx => {
|
|
const data = new Uint8Array([1, 2, 3]);
|
|
const result = ctx.producer.call('bin', transfer(data, [data.buffer]));
|
|
|
|
expect(ctx.postMessage.mock.calls[0][1].transfer[0]).toBeInstanceOf(
|
|
ArrayBuffer
|
|
);
|
|
|
|
// fake consumer return
|
|
ctx.handlers.return({
|
|
type: 'return',
|
|
id: 'bin:1',
|
|
data: new Uint8Array([3, 2, 1]),
|
|
});
|
|
|
|
await expect(result).resolves.toEqual(new Uint8Array([3, 2, 1]));
|
|
expect(data.byteLength).toBe(0);
|
|
});
|
|
|
|
it('should send optional payload call with abort signal', async ctx => {
|
|
const abortController = new AbortController();
|
|
const result = ctx.producer.call(
|
|
'init',
|
|
{ fastText: true },
|
|
abortController.signal
|
|
);
|
|
|
|
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
|
{
|
|
"id": "init:1",
|
|
"name": "init",
|
|
"payload": {
|
|
"fastText": true,
|
|
},
|
|
"type": "call",
|
|
}
|
|
`);
|
|
|
|
ctx.handlers.return({
|
|
type: 'return',
|
|
id: 'init:1',
|
|
data: { ok: true },
|
|
});
|
|
|
|
await expect(result).resolves.toEqual({ ok: true });
|
|
});
|
|
|
|
it('should send undefined payload for optional input call', async ctx => {
|
|
const result = ctx.producer.call('init', undefined);
|
|
|
|
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
|
{
|
|
"id": "init:1",
|
|
"name": "init",
|
|
"payload": undefined,
|
|
"type": "call",
|
|
}
|
|
`);
|
|
|
|
ctx.handlers.return({
|
|
type: 'return',
|
|
id: 'init:1',
|
|
data: { ok: true },
|
|
});
|
|
|
|
await expect(result).resolves.toEqual({ ok: true });
|
|
});
|
|
|
|
it('should cancel call', async ctx => {
|
|
const promise = ctx.producer.call('add', { a: 1, b: 2 });
|
|
|
|
promise.cancel();
|
|
|
|
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"id": "add:1",
|
|
"type": "cancel",
|
|
},
|
|
]
|
|
`);
|
|
|
|
await expect(promise).rejects.toThrow('canceled');
|
|
});
|
|
|
|
it('should timeout call', async ctx => {
|
|
const promise = ctx.producer.call('add', { a: 1, b: 2 });
|
|
|
|
vi.advanceTimersByTime(4000);
|
|
|
|
await expect(promise).rejects.toThrow('timeout');
|
|
});
|
|
|
|
it('should send subscribe op', async ctx => {
|
|
let ob = {
|
|
next: vi.fn(),
|
|
error: vi.fn(),
|
|
complete: vi.fn(),
|
|
};
|
|
|
|
// @ts-expect-error internal api
|
|
const subscriptions = ctx.producer.obs;
|
|
ctx.producer.ob$('sub', new Uint8Array([1, 2, 3])).subscribe(ob);
|
|
|
|
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
|
{
|
|
"id": "sub:1",
|
|
"name": "sub",
|
|
"payload": Uint8Array [
|
|
1,
|
|
2,
|
|
3,
|
|
],
|
|
"type": "subscribe",
|
|
}
|
|
`);
|
|
expect(subscriptions.has('sub:1')).toBe(true);
|
|
|
|
// fake consumer return
|
|
ctx.handlers.next({ type: 'next', id: 'sub:1', data: 1 });
|
|
ctx.handlers.next({ type: 'next', id: 'sub:1', data: 2 });
|
|
ctx.handlers.next({ type: 'next', id: 'sub:1', data: 3 });
|
|
|
|
expect(subscriptions.has('sub:1')).toBe(true);
|
|
|
|
ctx.handlers.complete({ type: 'complete', id: 'sub:1' });
|
|
|
|
expect(ob.next).toHaveBeenCalledTimes(3);
|
|
expect(ob.complete).toHaveBeenCalledTimes(1);
|
|
|
|
expect(subscriptions.has('sub:1')).toBe(false);
|
|
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"id": "sub:1",
|
|
"type": "unsubscribe",
|
|
},
|
|
]
|
|
`);
|
|
|
|
// smoking
|
|
ob = {
|
|
next: vi.fn(),
|
|
error: vi.fn(),
|
|
complete: vi.fn(),
|
|
};
|
|
ctx.producer.ob$('sub', new Uint8Array([1, 2, 3])).subscribe(ob);
|
|
|
|
expect(subscriptions.has('sub:2')).toBe(true);
|
|
|
|
ctx.handlers.next({ type: 'next', id: 'sub:2', data: 1 });
|
|
ctx.handlers.error({
|
|
type: 'error',
|
|
id: 'sub:2',
|
|
error: new Error('test'),
|
|
});
|
|
|
|
expect(ob.next).toHaveBeenCalledTimes(1);
|
|
expect(ob.error).toHaveBeenCalledTimes(1);
|
|
|
|
expect(subscriptions.has('sub')).toBe(false);
|
|
});
|
|
|
|
it('should transfer transferables with subscribe op', async ctx => {
|
|
const data = new Uint8Array([1, 2, 3]);
|
|
const sub = ctx.producer
|
|
.ob$('bin', transfer(data, [data.buffer]))
|
|
.subscribe({
|
|
next: vi.fn(),
|
|
});
|
|
|
|
expect(data.byteLength).toBe(0);
|
|
|
|
sub.unsubscribe();
|
|
});
|
|
|
|
it('should unsubscribe subscription op', ctx => {
|
|
const sub = ctx.producer.ob$('sub', new Uint8Array([1, 2, 3])).subscribe({
|
|
next: vi.fn(),
|
|
});
|
|
|
|
sub.unsubscribe();
|
|
|
|
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"id": "sub:1",
|
|
"type": "unsubscribe",
|
|
},
|
|
]
|
|
`);
|
|
});
|
|
});
|