/* global window, HTMLElement, location, RequestInit */ import { expect, test } from '@playwright/test'; import type { MathJSLabExecutor, MathJSLabExecutionResult } from 'mathjslab-markdown/mathjslab'; declare global { interface Window { mathJSLabWorkerTest: { executor: MathJSLabExecutor; viewer: HTMLElement & { markdown: string }; effects: Array<{ type: string; milliseconds?: number; reference?: string }>; workerCount: number; probe(): Promise<{ interpreter: boolean; plots: boolean }>; nextEffect(): Promise<{ type: string }>; serialize( result: MathJSLabExecutionResult, ): Promise<{ status: string; outputs: Array<{ type: string; markup?: string; text?: string; request?: { data: Array<{ y: number[] }> } }>; error?: string }>; }; } } test.beforeEach(async ({ page }) => { await page.goto('/test/browser/fixtures/mathjslab-worker.html'); await expect(page.locator('markdown-document')).toHaveAttribute('state', 'ready', { timeout: 20_000 }); }); test('cancels a returned receiver index and recovers with a clean Worker', async ({ page }) => { const result = await page.evaluate(async () => { const harness = window.mathJSLabWorkerTest; const session = await harness.executor.createSession({}); const controller = new AbortController(); harness.effects.length = 0; try { const first = harness.nextEffect(); const pending = session.execute( 'function y=g(a), if a==2, pause(60); else, pause(0.01); end; if a==1, y=[10,20,30]; else, y=a; end; end; x=0; a=99; a=g(++x)(g(++x):end); plot(a)', { blockIndex: 0, statementIndex: 0, signal: controller.signal }, ); await first; await harness.nextEffect(); const contexts = await harness.probe(); controller.abort('cancel literal element'); const cancelled = await harness.serialize(await pending); const effects = [...harness.effects]; const recovery = await harness.serialize(await session.execute('exist("x","var")', { blockIndex: 1, statementIndex: 0 })); return { cancelled, recovery, effects, contexts, recoveredContexts: await harness.probe() }; } finally { await session.dispose?.(); } }); expect(result.cancelled).toMatchObject({ status: 'cancelled', outputs: [] }); expect(result.effects).toEqual([ { type: 'delay', milliseconds: 10 }, { type: 'delay', milliseconds: 60000 }, ]); expect(result.recovery.outputs[0]?.markup).toContain('0'); expect(result.contexts).toMatchObject({ interpreter: false, plots: false }); expect(result.recoveredContexts).toMatchObject({ interpreter: false, plots: false }); }); test('retains returned receiver effects when indexing fails', async ({ page }) => { const result = await page.evaluate(async () => { const harness = window.mathJSLabWorkerTest; const session = await harness.executor.createSession({}); harness.effects.length = 0; try { const failed = await harness.serialize( await session.execute('function y=g(a), global count; count+=1; pause(a/1000); if a==1, y=[10,20]; else, y=a; end; end; global count; count=0; a=99; a=g(1)(g(3)); plot(a)', { blockIndex: 0, statementIndex: 0, }), ); const workspace = await harness.serialize(await session.execute('global count; a+count', { blockIndex: 1, statementIndex: 0 })); return { failed, workspace, effects: [...harness.effects], contexts: await harness.probe() }; } finally { await session.dispose?.(); } }); expect(result.failed.status).toBe('error'); expect(result.failed.outputs).toEqual([]); expect(result.workspace.outputs[0]?.markup).toContain('101'); expect(result.effects).toEqual([1, 3].map((milliseconds) => ({ type: 'delay', milliseconds }))); expect(result.contexts).toMatchObject({ interpreter: false, plots: false }); }); test('propagates a returned receiver index host failure without replay', async ({ page }) => { const result = await page.evaluate(async () => { const harness = window.mathJSLabWorkerTest; const session = await harness.executor.createSession({}); const original = window.setTimeout; window.setTimeout = new Proxy(original, { apply(target, receiver, args) { if (args[1] === 77) throw new Error('literal host failure'); return Reflect.apply(target, receiver, args); }, }); harness.effects.length = 0; try { const execution = await harness.serialize( await session.execute( 'function y=g(a), global count; count+=1; pause(a/1000); if a==1, y=[10,20]; else, y=a; end; end; global count; count=0; a=99; try, a=g(1)(g(77)); catch, end; a+count', { blockIndex: 0, statementIndex: 0 }, ), ); return { execution, effects: [...harness.effects], contexts: await harness.probe() }; } finally { window.setTimeout = original; await session.dispose?.(); } }); expect(result.execution.status).toBe('success'); expect(result.execution.outputs[0]?.markup).toContain('101'); expect(result.effects).toEqual([ { type: 'delay', milliseconds: 1 }, { type: 'delay', milliseconds: 77 }, ]); expect(result.contexts).toMatchObject({ interpreter: false, plots: false }); }); test('keeps numeric session configuration through a returned receiver wait', async ({ page }) => { const result = await page.evaluate(async () => { const harness = window.mathJSLabWorkerTest; const first = await harness.executor.createSession({}); const second = await harness.executor.createSession({}); const context = { blockIndex: 0, statementIndex: 0 }; try { await first.execute('configure("precision",20)', context); const effect = harness.nextEffect(); const pending = first.execute('function y=g(), pause(0.1); y={getconfig("precision")}; end; g(){1}', context); await effect; await second.execute('configure("precision",40)', context); const resumed = await harness.serialize(await pending); const other = await harness.serialize(await second.execute('getconfig("precision")', context)); return { resumed, other }; } finally { await first.dispose?.(); await second.dispose?.(); } }); expect(result.resumed.status).toBe('success'); expect(result.resumed.outputs[0]?.markup).toContain('20'); expect(result.other.outputs[0]?.markup).toContain('40'); }); test('plots a retained returned receiver with end and dynamic field order', async ({ page }) => { const result = await page.evaluate(async () => { const h = window.mathJSLabWorkerTest; const session = await h.executor.createSession({}); h.effects.length = 0; try { const source = 'function y=make(a), global trace; trace=trace*10+a; pause(a/1000); y=struct("a",[10,20,30]); end; function y=field(), global trace; trace=trace*10+3; pause(0.003); y="a"; end; function y=idx(a), global trace; trace=trace*10+a; pause(a/1000); y=a; end; global trace; trace=0; x=0; plot(make(++x).(field())(idx(++x):end)); trace+x'; const execution = await h.serialize(await session.execute(source, { blockIndex: 0, statementIndex: 0 })); return { execution, effects: [...h.effects], contexts: await h.probe() }; } finally { await session.dispose?.(); } }); expect(result.execution.status).toBe('success'); expect(result.execution.outputs.find((o) => o.type === 'plot')?.request?.data.map((s) => s.y)).toEqual([[20, 30]]); expect(result.execution.outputs.find((o) => o.type === 'mathml')?.markup).toContain('314'); expect(result.effects).toEqual([3, 1, 2].map((milliseconds) => ({ type: 'delay', milliseconds }))); expect(result.contexts).toMatchObject({ interpreter: false, plots: false }); }); test('expands returned cell receivers and dispatches returned handles', async ({ page }) => { const result = await page.evaluate(async () => { const h = window.mathJSLabWorkerTest; const session = await h.executor.createSession({}); h.effects.length = 0; try { const execution = await h.serialize( await session.execute( 'function y=make(), pause(0.001); y={struct("a",10),struct("a",20)}; end; function y=g(a), pause(a/1000); y=a; end; function y=makehandle(), pause(0.002); y=@g; end; [a,b]=make(){:}.a; a+b+makehandle()(3)', { blockIndex: 0, statementIndex: 0 }, ), ); return { execution, effects: [...h.effects], contexts: await h.probe() }; } finally { await session.dispose?.(); } }); expect(result.execution.status).toBe('success'); expect(result.execution.outputs[0]?.markup).toContain('33'); expect(result.effects).toEqual([1, 2, 3].map((milliseconds) => ({ type: 'delay', milliseconds }))); expect(result.contexts).toMatchObject({ interpreter: false, plots: false }); }); test('uses returned receivers in plots across Markdown blocks', async ({ page }) => { await page.evaluate(() => { const harness = window.mathJSLabWorkerTest; harness.effects.length = 0; harness.viewer.markdown = '\n```mathjslab\nglobal count; count=0\nfunction y=sample(v)\nglobal count; count+=1; pause(0.001); y=v;\nend\nfunction y=samples(), y=[sample(1),sample(2)]; end\n```\n\n```mathjslab\nplot(samples()(1:end))\n```\n\n```mathjslab\ncount\n```'; }); const viewer = page.locator('markdown-document'); await expect(viewer.locator('.plot-container')).toHaveCount(1, { timeout: 20_000 }); await expect(viewer).toHaveAttribute('state', 'ready', { timeout: 20_000 }); await expect(viewer.locator('.mathjslab-result[data-state="bad"]')).toHaveCount(0); await expect(viewer.locator('.mathjslab-result').last()).toContainText('2'); const request = JSON.parse((await viewer.locator('.plot-container').getAttribute('data-request')) ?? '{}'); expect(request.data.map((series: { y: number[] }) => series.y)).toEqual([[1, 2]]); expect(await page.evaluate(() => window.mathJSLabWorkerTest.effects)).toEqual([ { type: 'delay', milliseconds: 1 }, { type: 'delay', milliseconds: 1 }, ]); });