import { bootstrapFrontend, type FrontendCloseRequest, } from '../src/lib/frontendBootstrap.ts'; const assertEquals = (actual: T, expected: T, message: string) => { if (actual !== expected) { throw new Error(`${message}: expected ${expected}, got ${actual}`); } }; const deferred = () => { let resolve!: (value: T) => void; let reject!: (reason: unknown) => void; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, resolve, reject }; }; Deno.test('close-listener registration failure destroys the empty window without mounting hooks', async () => { const registrationFailure = new Error('registration failed'); const destructionStarted = deferred(); const destruction = deferred(); const order: string[] = []; const reported: unknown[] = []; let renders = 0; let settled = false; const bootstrap = bootstrapFrontend({ registerCloseRequested: () => Promise.reject(registrationFailure), disposeOwners: () => { order.push('dispose owners'); return Promise.resolve(); }, drainPersistence: () => { order.push('drain persistence'); return Promise.resolve(); }, destroyWindow: () => { order.push('destroy window'); destructionStarted.resolve(); return destruction.promise; }, render: () => { renders += 1; }, reportFailure: (_message, error) => reported.push(error), }).then(result => { settled = true; return result; }); await destructionStarted.promise; assertEquals(renders, 0, 'React and its hooks must remain unmounted'); assertEquals(settled, false, 'bootstrap must explicitly await window destruction'); assertEquals( JSON.stringify(order), JSON.stringify(['dispose owners', 'drain persistence', 'destroy window']), 'admissions must close and drain before destroying the empty window', ); destruction.resolve(); assertEquals(await bootstrap, false, 'registration failure must report no render'); assertEquals(renders, 0, 'hooks must never mount after failure cleanup'); assertEquals(reported.length, 1, 'the registration failure must be handled once'); assertEquals(reported[0], registrationFailure, 'the original failure must be reported'); }); Deno.test('bootstrap handles failure of the fail-closed destroy action', async () => { const registrationFailure = new Error('registration failed'); const destructionFailure = new Error('destroy failed'); const reported: unknown[] = []; let renders = 0; const rendered = await bootstrapFrontend({ registerCloseRequested: () => Promise.reject(registrationFailure), disposeOwners: () => Promise.resolve(), drainPersistence: () => Promise.resolve(), destroyWindow: () => Promise.reject(destructionFailure), render: () => { renders += 1; }, reportFailure: (_message, error) => reported.push(error), }); assertEquals(rendered, false, 'a failed registration must never render'); assertEquals(renders, 0, 'destroy failure must not fall back to mounting hooks'); assertEquals(reported.length, 2, 'both terminal failures must be handled'); assertEquals(reported[0], registrationFailure, 'registration failure must be preserved'); assertEquals(reported[1], destructionFailure, 'destroy failure must be observed'); }); Deno.test('successful bootstrap mounts only after close registration resolves', async () => { const registered = deferred<(event: FrontendCloseRequest) => void | Promise>(); const registrationAcknowledged = deferred<() => void>(); let renders = 0; const bootstrap = bootstrapFrontend({ registerCloseRequested: handler => { registered.resolve(handler); return registrationAcknowledged.promise; }, disposeOwners: () => Promise.resolve(), drainPersistence: () => Promise.resolve(), destroyWindow: () => Promise.resolve(), render: () => { renders += 1; }, reportFailure: () => {}, }); await registered.promise; assertEquals(renders, 0, 'render must wait for registration acknowledgement'); registrationAcknowledged.resolve(() => {}); assertEquals(await bootstrap, true, 'successful registration should render the app'); assertEquals(renders, 1, 'the app should mount once'); }); Deno.test('close before registration acknowledgement lets Tauri destroy after the drain', async () => { const registered = deferred<(event: FrontendCloseRequest) => void | Promise>(); const registrationAcknowledged = deferred<() => void>(); const order: string[] = []; let renders = 0; let prevented = 0; let listenerCleanups = 0; const bootstrap = bootstrapFrontend({ registerCloseRequested: handler => { registered.resolve(handler); return registrationAcknowledged.promise; }, disposeOwners: () => { order.push('dispose owners'); return Promise.resolve(); }, drainPersistence: () => { order.push('drain persistence'); return Promise.resolve(); }, destroyWindow: () => { order.push('destroy window'); return Promise.resolve(); }, render: () => { renders += 1; }, reportFailure: () => {}, }); const handler = await registered.promise; const close = Promise.resolve(handler({ preventDefault: () => prevented += 1, })).then(() => { if (prevented === 0) order.push('tauri destroy'); }); assertEquals(prevented, 0, 'the first close must remain eligible for Tauri destruction'); assertEquals(renders, 0, 'React must remain unmounted while registration is unresolved'); registrationAcknowledged.resolve(async () => { listenerCleanups += 1; }); await close; assertEquals(await bootstrap, false, 'a pre-render close must report no mounted app'); assertEquals(renders, 0, 'a closing realm must never mount React'); assertEquals(listenerCleanups, 0, 'normal close leaves listener teardown to webview destruction'); assertEquals( JSON.stringify(order), JSON.stringify(['dispose owners', 'drain persistence', 'tauri destroy']), 'Tauri must destroy the first close after frontend state has drained', ); }); Deno.test('a repeated close cannot bypass the first close drain', async () => { const registered = deferred<(event: FrontendCloseRequest) => void | Promise>(); const ownerDrained = deferred(); const persistenceDrained = deferred(); let renders = 0; let firstPrevented = 0; let secondPrevented = 0; let tauriDestroys = 0; let explicitDestroys = 0; let listenerCleanups = 0; const bootstrap = bootstrapFrontend({ registerCloseRequested: handler => { registered.resolve(handler); return Promise.resolve(() => { listenerCleanups += 1; }); }, disposeOwners: () => ownerDrained.promise, drainPersistence: () => persistenceDrained.promise, destroyWindow: () => { explicitDestroys += 1; return Promise.resolve(); }, render: () => { renders += 1; }, reportFailure: () => {}, }); const handler = await registered.promise; assertEquals(await bootstrap, true, 'the test window should mount before close'); const firstClose = Promise.resolve(handler({ preventDefault: () => firstPrevented += 1, })).then(() => { if (firstPrevented === 0) tauriDestroys += 1; }); const secondClose = Promise.resolve(handler({ preventDefault: () => secondPrevented += 1, })).then(() => { if (secondPrevented === 0) tauriDestroys += 1; }); assertEquals(firstPrevented, 0, 'the first close must own automatic destruction'); assertEquals(secondPrevented, 1, 'a repeated close must remain intercepted'); assertEquals(tauriDestroys, 0, 'destruction must wait for both frontend drains'); assertEquals(listenerCleanups, 0, 'the active close listener must remain installed'); ownerDrained.resolve(); await Promise.resolve(); assertEquals(tauriDestroys, 0, 'persistence must also settle before destruction'); persistenceDrained.resolve(); await Promise.all([firstClose, secondClose]); assertEquals(renders, 1, 'the mounted application should not render again during close'); assertEquals(tauriDestroys, 1, 'only the first close may trigger Tauri destruction'); assertEquals(explicitDestroys, 0, 'normal close must use the Tauri close callback path'); assertEquals(listenerCleanups, 0, 'webview destruction owns normal listener teardown'); }); Deno.test('render failure drains and unregisters before handled window destruction', async () => { const renderFailure = new Error('render failed'); const unlistenFailure = new Error('unlisten failed'); const destroyFailure = new Error('destroy failed'); const ownerDrained = deferred(); const persistenceDrained = deferred(); const drainsStarted = deferred(); const finalizerStarted = deferred(); const order: string[] = []; const reported: unknown[] = []; let cleanups = 0; const bootstrap = bootstrapFrontend({ registerCloseRequested: () => { order.push('register'); return Promise.resolve(async () => { order.push('unlisten'); cleanups += 1; finalizerStarted.resolve(); throw unlistenFailure; }); }, disposeOwners: async () => { order.push('dispose owners'); await ownerDrained.promise; }, drainPersistence: async () => { order.push('drain persistence'); drainsStarted.resolve(); await persistenceDrained.promise; }, destroyWindow: () => { order.push('destroy window'); return Promise.reject(destroyFailure); }, render: () => { order.push('render'); throw renderFailure; }, reportFailure: (_message, error) => reported.push(error), }); await drainsStarted.promise; assertEquals(cleanups, 0, 'listener cleanup must wait for frontend drains'); assertEquals( JSON.stringify(order), JSON.stringify(['register', 'render', 'dispose owners', 'drain persistence']), 'render failure must synchronously start both frontend drains', ); ownerDrained.resolve(); await Promise.resolve(); assertEquals( order.includes('destroy window'), false, 'window destruction must wait for persistence as well as owner cleanup', ); persistenceDrained.resolve(); await finalizerStarted.promise; assertEquals(cleanups, 1, 'the installed close listener must unregister exactly once'); assertEquals(await bootstrap, false, 'a render failure must be fully handled'); assertEquals( JSON.stringify(order), JSON.stringify([ 'register', 'render', 'dispose owners', 'drain persistence', 'unlisten', 'destroy window', ]), 'destruction must be the terminal finalizer', ); assertEquals(reported.length, 3, 'render, cleanup, and destroy failures must be observed'); assertEquals(reported[0], renderFailure, 'the render failure must be reported first'); assertEquals(reported[1], unlistenFailure, 'listener cleanup rejection must be reported'); assertEquals(reported[2], destroyFailure, 'window destroy rejection must be reported'); });