From 122577f6bd75035a2cf3adb4c27f3cef39041389 Mon Sep 17 00:00:00 2001 From: Zainab Travadi Date: Mon, 22 Jun 2026 12:53:30 +0000 Subject: [PATCH 1/2] fix(jsx): log unmount errors instead of silently swallowing them --- packages/jsx/src/render.ts | 14 +++++++++----- packages/jsx/src/unmountHelpers.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 packages/jsx/src/unmountHelpers.test.ts diff --git a/packages/jsx/src/render.ts b/packages/jsx/src/render.ts index 0574e79c..a2c871be 100644 --- a/packages/jsx/src/render.ts +++ b/packages/jsx/src/render.ts @@ -15,13 +15,17 @@ import { createElement } from './createElement.js'; import { setCurrentApp } from './runtime.js'; /** - * Shared inline unmount helper — used by both the dev-server-backed path - * and the fallback path so cleanup logic stays synchronized. + * Unmount a list of apps. Swallow any errors thrown by `unmount()` but log + * a single error message for observability. Exported only for tests. */ -function _unmountApps(apps: Array<{ unmount?: () => void }>): void { +export function unmountApps(apps: Array<{ unmount?: () => void }>): void { apps.forEach((app) => { if (typeof app.unmount === 'function') { - try { app.unmount(); } catch {} + try { + app.unmount(); + } catch (err) { + console.error('[jsx] Error during unmount():', err); + } } }); } @@ -162,7 +166,7 @@ export async function render( // dev-server unavailable — use local helper for cleanup const apps = (globalThis as any).__termuijs_apps; if (Array.isArray(apps)) { - _unmountApps(apps); + unmountApps(apps); (globalThis as any).__termuijs_apps = []; } }); diff --git a/packages/jsx/src/unmountHelpers.test.ts b/packages/jsx/src/unmountHelpers.test.ts new file mode 100644 index 00000000..054e2a13 --- /dev/null +++ b/packages/jsx/src/unmountHelpers.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect, vi } from 'vitest'; +import { unmountApps } from './render.js'; + +describe('unmountApps', () => { + it('logs errors, completes cleanup, and does not rethrow', () => { + const err = new Error('boom'); + const throwing = { unmount: vi.fn(() => { throw err; }) }; + const called = { unmount: vi.fn(() => { /* ok */ }) }; + + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + // Should not throw + expect(() => unmountApps([throwing, called])).not.toThrow(); + + // Ensure the second unmount still ran + expect(called.unmount).toHaveBeenCalledTimes(1); + + // Ensure we logged the error exactly once with the expected message + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('[jsx] Error during unmount():', err); + + spy.mockRestore(); + }); +}); From 89761169470d2ce84f38469bed77f237b7bad514 Mon Sep 17 00:00:00 2001 From: Zainab Travadi Date: Mon, 22 Jun 2026 13:04:24 +0000 Subject: [PATCH 2/2] test(jsx): restore console.error spy in finally block --- packages/jsx/src/unmountHelpers.test.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/jsx/src/unmountHelpers.test.ts b/packages/jsx/src/unmountHelpers.test.ts index 054e2a13..c0d848ab 100644 --- a/packages/jsx/src/unmountHelpers.test.ts +++ b/packages/jsx/src/unmountHelpers.test.ts @@ -9,16 +9,18 @@ describe('unmountApps', () => { const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); - // Should not throw - expect(() => unmountApps([throwing, called])).not.toThrow(); + try { + // Should not throw + expect(() => unmountApps([throwing, called])).not.toThrow(); - // Ensure the second unmount still ran - expect(called.unmount).toHaveBeenCalledTimes(1); + // Ensure the second unmount still ran + expect(called.unmount).toHaveBeenCalledTimes(1); - // Ensure we logged the error exactly once with the expected message - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith('[jsx] Error during unmount():', err); - - spy.mockRestore(); + // Ensure we logged the error exactly once with the expected message + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('[jsx] Error during unmount():', err); + } finally { + spy.mockRestore(); + } }); });