From 9062d4c5c3aec904a78f5b5a064f0b52da84d543 Mon Sep 17 00:00:00 2001 From: Loren Segal Date: Sat, 3 Oct 2026 14:17:08 -0700 Subject: [PATCH 1/3] Start work on issue #5 [skip ci] From ced8843cb292bd84b1e5904fdd088ae814ee3454 Mon Sep 17 00:00:00 2001 From: Loren Segal Date: Sat, 3 Oct 2026 14:21:27 -0700 Subject: [PATCH 2/3] Checkpoint issue #5 progress --- app/src/App.tsx | 7 ++++- app/src/embed.test.ts | 59 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/app/src/App.tsx b/app/src/App.tsx index cc94ce5..abc8824 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -347,7 +347,12 @@ function App({ config: configOverrides }: { config?: Partial } = {} let active = true; let opened: LocalProject | null = null; void (async () => { - const loadedIdentity = await loadIdentity(config); + // Identity loading can't be cancelled, so ignore its result (or failure) after unmount. + const loadedIdentity = await loadIdentity(config).catch((error) => { + if (active) throw error; + return null; + }); + if (!active || !loadedIdentity) return; const loadedProject = await LocalProject.open( loadedIdentity, config, diff --git a/app/src/embed.test.ts b/app/src/embed.test.ts index 0ca3bdc..a5e7a09 100644 --- a/app/src/embed.test.ts +++ b/app/src/embed.test.ts @@ -1,9 +1,34 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { defineCustomElement, mount } from "./embed"; +import type { DeviceIdentity } from "./lib/identity"; +import { LocalProject } from "./lib/project"; + +// Track every identity load App starts so each test can wait for them to settle; +// otherwise they outlive the test and touch jsdom globals after teardown. +const identityLoads = vi.hoisted(() => ({ + pending: new Set>(), + override: null as (() => Promise) | null, +})); + +vi.mock("./lib/identity", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadIdentity: (...args: Parameters) => { + const load = identityLoads.override?.() ?? actual.loadIdentity(...args); + identityLoads.pending.add(load); + return load; + }, + }; +}); const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); -afterEach(() => { +afterEach(async () => { + await Promise.allSettled(identityLoads.pending); + identityLoads.pending.clear(); + identityLoads.override = null; + vi.restoreAllMocks(); document.body.replaceChildren(); document.getElementById("subscript-write-styles")?.remove(); }); @@ -21,11 +46,12 @@ describe("embeddable library entry", () => { const otherHost = document.createElement("div"); document.body.append(otherHost); - mount(otherHost); + const otherHandle = mount(otherHost); await flush(); expect(document.querySelectorAll("#subscript-write-styles")).toHaveLength(1); handle.unmount(); + otherHandle.unmount(); expect(host.querySelector(".subscript-write")).toBeNull(); }); @@ -40,4 +66,31 @@ describe("embeddable library entry", () => { element.remove(); expect(element.querySelector(".subscript-write")).toBeNull(); }); + + it("ignores identity loading that settles after unmount", async () => { + let failLoad: (error: Error) => void = () => undefined; + identityLoads.override = () => + new Promise((_, reject) => { + failLoad = reject; + }); + const open = vi.spyOn(LocalProject, "open"); + const unhandled = vi.fn(); + process.on("unhandledRejection", unhandled); + + try { + const host = document.createElement("div"); + document.body.append(host); + const handle = mount(host); + await flush(); + handle.unmount(); + + failLoad(new ReferenceError("localStorage is not defined")); + await flush(); + + expect(open).not.toHaveBeenCalled(); + expect(unhandled).not.toHaveBeenCalled(); + } finally { + process.off("unhandledRejection", unhandled); + } + }); }); From 4b2472c121eb606ba9f750912a047a8811cb6b71 Mon Sep 17 00:00:00 2001 From: Loren Segal Date: Sat, 3 Oct 2026 14:25:16 -0700 Subject: [PATCH 3/3] Fix flaky embed tests: identity loading outlives jsdom teardown Closes #5