import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; import { stub } from "@std/testing/mock"; import { expect } from "@std/expect"; import { Identity } from "@commonfabric/identity"; import { RequestType } from "@commonfabric/runtime-client"; import { Browser } from "../browser.ts"; import type { Page } from "../page.ts"; import { assertShellDocument, describeShellPage, readAndDescribeShellPage, readShellPageProbe, type ShellPageProbe, } from "../shell-page-probe.ts"; import { describeStateWaitFailure, disposePageRuntime, login, waitForShellReady, } from "../shell-utils.ts"; import { describeConditionWaitFailure, waitForCondition } from "../utils.ts"; // What the toolshed answers with when its fetch to the shell dev server fails. const PROXY_FAILURE_TEXT = "Failed to proxy to http://localhost:6000/. Is the shell dev server running?"; // The part of the shell's entry document that matters here: a title, and the // root element every shell wait afterwards depends on. const SHELL_DOCUMENT = ` Common Fabric `; // The same document with the shell already booted far enough to have published // itself, holding the home view and an identity named by its DID. const BOOTED_SHELL_DOCUMENT = ` Common Fabric `; // A booted shell holding a runtime with two requests in flight, reported // oldest first, which is the order the runtime keeps them in. The request // types are the `RequestType` values a real report carries, which the // assertions below name through the enum so that a change to either is caught // here rather than diverging quietly. // // Its `dispose()` rejects, which is the cheapest way to reach the teardown's // catch; what a teardown failure arrives as does not change what the catch // then reports. const UNRESPONSIVE_RUNTIME_DOCUMENT = ` Common Fabric `; // A booted shell holding a runtime that answers: nothing is in flight, and a // disposal returns. const IDLE_RUNTIME_DOCUMENT = ` Common Fabric `; // A booted shell whose runtime does not report what it is waiting on, and one // whose report throws. const SILENT_RUNTIME_DOCUMENT = ` Common Fabric `; const REFUSING_RUNTIME_DOCUMENT = ` Common Fabric `; // A booted shell whose runtime's worker has logged three kinds of warning or // error, two of them with the same number of errors, and one message at `debug` // only, which is neither. The counts are shaped as the worker // reports them: by logger, then by message, with `total` beside each level. const LOGGING_RUNTIME_DOCUMENT = ` Common Fabric `; // A booted shell whose runtime still reports its requests, but whose worker // never answers the request for its logs, and one whose request is refused. const WEDGED_WORKER_DOCUMENT = ` Common Fabric `; const REFUSING_WORKER_DOCUMENT = ` Common Fabric `; // A booted shell whose page answers until it is asked for the worker's logs, // and then holds its main thread for good. const WEDGING_WORKER_READ_DOCUMENT = ` Common Fabric `; // A booted shell whose `setIdentity` refuses. The login reaches the page and // fails there, which is the failure a report has to describe. const REFUSING_SHELL_DOCUMENT = ` Common Fabric `; function handle(request: Request): Response { const { pathname } = new URL(request.url); switch (pathname) { case "/proxy-failure": return new Response(PROXY_FAILURE_TEXT, { status: 502, headers: { "content-type": "text/plain; charset=utf-8" }, }); case "/shell": return new Response(SHELL_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); case "/booted-shell": return new Response(BOOTED_SHELL_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); case "/refusing-shell": return new Response(REFUSING_SHELL_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); case "/unresponsive-runtime": return new Response(UNRESPONSIVE_RUNTIME_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); case "/idle-runtime": return new Response(IDLE_RUNTIME_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); case "/silent-runtime": return new Response(SILENT_RUNTIME_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); case "/refusing-runtime": return new Response(REFUSING_RUNTIME_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); case "/logging-runtime": return new Response(LOGGING_RUNTIME_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); case "/wedged-worker": return new Response(WEDGED_WORKER_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); case "/wedging-worker-read": return new Response(WEDGING_WORKER_READ_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); case "/refusing-worker": return new Response(REFUSING_WORKER_DOCUMENT, { headers: { "content-type": "text/html; charset=utf-8" }, }); } return new Response("not found", { status: 404 }); } // `describeShellPage` rendered over a page holding nothing remarkable, split // into its lines, with `fields` deciding what the case is about. The default // carries a runtime, so a case about the absence of one says so. function describedLines(fields: Partial): string[] { return describeShellPage({ url: "http://localhost/shell", title: "Common Fabric", rootView: true, app: true, runtime: true, text: "", consoleTail: [], ...fields, }).split("\n"); } // The message of the error `work` rejects with. Fails the test when it // resolves, so a check on the message cannot pass vacuously. async function rejectionMessage(work: Promise): Promise { try { await work; } catch (error) { return error instanceof Error ? error.message : String(error); } throw new Error("Expected the call to throw, and it returned instead."); } /** The variable the wait reads its safety net's length from. */ const NET_VARIABLE = "CF_WAIT_FOR_CONDITION_TIMEOUT_MS"; // The stuck-condition safety net is five minutes, which no test can sit // through. This drives it through the environment variable the wait reads for // exactly that, and returns the message it gave up with. Everything past the // first line of that message is what the wait assembles, which nothing else // exercises. async function shortNetRejectionMessage( work: () => Promise, ): Promise { // Put back whatever the run was started with, rather than removing it: // somebody running this file with a net of their own chose that, and a // test that dropped it would change every wait after itself. const before = Deno.env.get(NET_VARIABLE); Deno.env.set(NET_VARIABLE, "1500"); try { return await rejectionMessage(work()); } finally { if (before === undefined) Deno.env.delete(NET_VARIABLE); else Deno.env.set(NET_VARIABLE, before); } } describe("shell-failure-reports", () => { let server: Deno.HttpServer; let origin: string; let browser: Browser; let page: Page; beforeAll(async () => { server = Deno.serve({ port: 0, onListen: () => {} }, handle); origin = `http://localhost:${(server.addr as Deno.NetAddr).port}`; browser = await Browser.launch(); page = await browser.newPage(); }); afterAll(async () => { await page.close(); await browser.close(); await server.shutdown(); }); // Loads `path` from the local server and installs the console formatter, // which is the state `ShellIntegration.goto` leaves a page in before it // starts waiting. const load = async (path: string): Promise => { const url = `${origin}${path}`; await page.goto(url); await page.applyConsoleFormatter(); return url; }; describe("readShellPageProbe()", () => { it("returns the status and text of a document that is not the shell", async () => { await load("/proxy-failure"); const probe = await readShellPageProbe(page); expect(probe.rootView).toBe(false); expect(probe.app).toBe(false); expect(probe.status).toBe(502); expect(probe.text).toBe(PROXY_FAILURE_TEXT); }); it("returns the title and root element of the shell's own document", async () => { await load("/shell"); const probe = await readShellPageProbe(page); expect(probe.rootView).toBe(true); expect(probe.title).toBe("Common Fabric"); expect(probe.status).toBe(200); // Nothing has booted, so nothing has published `globalThis.app`. expect(probe.app).toBe(false); }); it("returns the view and identity a booted shell holds", async () => { await load("/booted-shell"); const probe = await readShellPageProbe(page); expect(probe.app).toBe(true); expect(probe.view).toEqual({ builtin: "home" }); expect(probe.identityDid).toBe("did:key:zBootedShellFixture"); }); it("returns the requests the page's runtime is still waiting on", async () => { await load("/unresponsive-runtime"); const probe = await readShellPageProbe(page); expect(probe.runtime).toBe(true); expect(probe.pendingRequests).toEqual([ { msgId: 7, type: RequestType.Dispose, ageMs: 41203 }, { msgId: 9, type: RequestType.CellSubscribe, ageMs: 84 }, ]); expect(probe.pendingRequestsError).toBeUndefined(); }); it("returns an empty list for a runtime with nothing in flight", async () => { await load("/idle-runtime"); const probe = await readShellPageProbe(page); expect(probe.runtime).toBe(true); // An empty list and an absent one are different answers, and the // boundary keeps them apart: a page's `undefined` property is dropped on // the way back, while an empty array survives as one. expect(probe.pendingRequests).toEqual([]); }); it("returns no runtime for a page that carries none", async () => { await load("/booted-shell"); const probe = await readShellPageProbe(page); expect(probe.runtime).toBe(false); expect(probe.pendingRequests).toBeUndefined(); }); it("returns a runtime that does not report its requests", async () => { await load("/silent-runtime"); const probe = await readShellPageProbe(page); expect(probe.runtime).toBe(true); expect(probe.pendingRequests).toBeUndefined(); expect(probe.pendingRequestsError).toBeUndefined(); }); it("returns the reason when the runtime refuses to report them", async () => { await load("/refusing-runtime"); const probe = await readShellPageProbe(page); expect(probe.runtime).toBe(true); expect(probe.pendingRequests).toBeUndefined(); expect(probe.pendingRequestsError).toContain("the connection is gone"); }); it("returns the worker's warnings and errors, most errors then most warnings first", async () => { await load("/logging-runtime"); const probe = await readShellPageProbe(page); expect(probe.workerProblems).toEqual([ { logger: "storage.v2", message: "sync-load-failure", warn: 1, error: 3, }, { logger: "runner", message: "action-failed", warn: 4, error: 2 }, { logger: "scheduler", message: "schedule-error", warn: 0, error: 2 }, ]); expect(probe.workerProblemsError).toBeUndefined(); }); it("returns an empty list for a worker that has logged no warning or error", async () => { await load("/idle-runtime"); const probe = await readShellPageProbe(page); expect(probe.workerProblems).toEqual([]); }); it("returns no worker logs for a runtime that does not report them", async () => { await load("/silent-runtime"); const probe = await readShellPageProbe(page); expect(probe.workerProblems).toBeUndefined(); expect(probe.workerProblemsError).toBeUndefined(); }); it("returns the rest of the probe when the worker never answers", async () => { await load("/wedged-worker"); const probe = await readShellPageProbe(page, { workerBudgetMs: 100 }); expect(probe.workerProblems).toBeUndefined(); expect(probe.workerProblemsError).toContain( "the worker did not answer within 100ms", ); expect(probe.pendingRequests).toEqual([ { msgId: 3, type: RequestType.GetLoggerCounts, ageMs: 1200 }, ]); }); it("returns the reason when the worker refuses to report its logs", async () => { await load("/refusing-worker"); const probe = await readShellPageProbe(page); expect(probe.workerProblems).toBeUndefined(); expect(probe.workerProblemsError).toContain("the worker has gone"); }); it("returns the page it read when the page stops answering during the worker read", async () => { // Its own browser, because the wedge below is permanent: the page's main // thread is held for good once the worker's logs are asked for. const wedged = await Browser.launch(); try { const wedgedPage = await wedged.newPage( `${origin}/wedging-worker-read`, ); const probe = await readShellPageProbe(wedgedPage, { workerBudgetMs: 100, }); expect(probe.rootView).toBe(true); expect(probe.pendingRequests).toEqual([]); expect(probe.workerProblemsError).toContain( "the page did not answer within 10100ms", ); } finally { await wedged.close(); } }); it("returns the console messages the page retained", async () => { await load("/booted-shell"); await page.evaluate(() => { console.warn("the page said something before the wait gave up"); }); const probe = await readShellPageProbe(page); expect(probe.consoleTail.length).toBe(1); expect(probe.consoleTail[0]).toContain( "[warn] the page said something before the wait gave up", ); }); }); describe("assertShellDocument()", () => { it("throws naming the document that loaded instead of the shell", async () => { const url = await load("/proxy-failure"); const message = await rejectionMessage(assertShellDocument(page, url)); expect(message).toContain( "the document that loaded is not the shell: it has no x-root-view", ); expect(message).toContain("response status: 502"); expect(message).toContain("globalThis.app: absent"); expect(message).toContain( "document text: Failed to proxy to http://localhost:6000/", ); }); it("returns for the shell's own document", async () => { const url = await load("/shell"); await assertShellDocument(page, url); }); }); describe("waitForShellReady()", () => { it("returns for a document that is not the shell", async () => { await load("/proxy-failure"); await waitForShellReady(page); }); it("returns for a shell that has published itself", async () => { await load("/booted-shell"); await waitForShellReady(page); }); it("names the shell whose bootstrap never published the handle", async () => { await load("/shell"); const message = await shortNetRejectionMessage(() => waitForShellReady(page) ); expect(message).toBe( "The shell never published itself on globalThis.app.", ); }); it("waits for publication announced after an earlier readiness event", async () => { await load("/shell"); await page.evaluate(() => { globalThis.setInterval = () => { throw new Error("The shell readiness wait must not poll."); }; const target: EventTarget = globalThis; const add = target.addEventListener.bind(target); const remove = target.removeEventListener.bind(target); const listeners = new Set(); let subscribed!: () => void; Object.defineProperty(globalThis, "shellReadinessSubscribed", { value: new Promise((resolve) => subscribed = resolve), }); target.addEventListener = (type, listener, options) => { add(type, listener, options); if (type === "cf-shell-ready" && listener) { listeners.add(listener); subscribed(); } }; target.removeEventListener = (type, listener, options) => { if (type === "cf-shell-ready" && listener) listeners.delete(listener); remove(type, listener, options); }; Object.defineProperty(globalThis, "shellReadinessListeners", { get: () => listeners.size, }); }); const ready = waitForShellReady(page); await Promise.race([ ready.then(() => { throw new Error("The shell was reported ready before publication."); }), page.evaluate(async () => { const fixture = globalThis as typeof globalThis & { shellReadinessSubscribed: Promise; }; await fixture.shellReadinessSubscribed; }), ]); await page.evaluate(() => { globalThis.dispatchEvent(new Event("cf-shell-ready")); }); const beforePublication = await page.evaluate(() => { const fixture = globalThis as typeof globalThis & { shellReadinessListeners: number; }; return { listeners: fixture.shellReadinessListeners, published: globalThis.app !== undefined, }; }); expect(beforePublication).toEqual({ listeners: 1, published: false }); await page.evaluate(() => { globalThis.app = { serialize: () => ({ view: { builtin: "home" } }), } as typeof globalThis.app; globalThis.dispatchEvent(new Event("cf-shell-ready")); }); await ready; const result = await page.evaluate(() => { const fixture = globalThis as typeof globalThis & { shellReadinessListeners: number; }; return { listeners: fixture.shellReadinessListeners, view: globalThis.app.serialize().view, }; }); expect(result.view).toEqual({ builtin: "home" }); expect(result.listeners).toBe(0); }); }); describe("describeConditionWaitFailure()", () => { it("returns a block naming the predicate, its arguments, and the page", async () => { await load("/booted-shell"); const described = await describeConditionWaitFailure( page, "(probe, slug) => probe.collect(slug).length === 1", ["members-7"], ); expect(described).toContain( "awaited condition: (probe, slug) => probe.collect(slug).length === 1", ); expect(described).toContain(' [0] "members-7"'); expect(described).toContain("x-root-view: present"); expect(described).toContain("did:key:zBootedShellFixture"); }); it("gives each argument its own line, so shared predicates differ", async () => { await load("/shell"); const described = await describeConditionWaitFailure( page, "(probe, ...rest) => rest", ["members-7", { member: "2" }], ); expect(described).toContain(" condition arguments:\n"); expect(described).toContain(' [0] "members-7"'); expect(described).toContain(' [1] {"member":"2"}'); }); it("spells out the numbers JSON would render as something else", async () => { await load("/shell"); // Rendered by JSON alone these read back as `null`, `null` and `0`, // so a wait told apart from its neighbours by one of them would be // told apart wrongly. const described = await describeConditionWaitFailure( page, "() => false", [NaN, Infinity, -0, { attempts: -Infinity }], ); expect(described).toContain(' [0] "NaN"'); expect(described).toContain(' [1] "Infinity"'); expect(described).toContain(' [2] "-0"'); expect(described).toContain(' [3] {"attempts":"-Infinity"}'); }); it("collapses a predicate written over several lines onto one", async () => { await load("/shell"); const described = await describeConditionWaitFailure( page, "(probe) => {\n return probe;\n}", ); expect(described).toContain( "awaited condition: (probe) => { return probe; }", ); expect(described).not.toContain("condition arguments:"); }); it("cuts a predicate at the length the report carries", async () => { await load("/shell"); const described = await describeConditionWaitFailure( page, `() => ${"x".repeat(600)}`, ); // Six characters of `() => ` precede the run, so the cut lands inside it. expect(described).toContain(`() => ${"x".repeat(394)}…\n`); expect(described).not.toContain("x".repeat(395)); }); it("cuts an argument at the same length", async () => { await load("/shell"); const described = await describeConditionWaitFailure( page, "() => false", ["y".repeat(600)], ); // A quote opens the rendered string, so 399 of the run reach the line. expect(described).toContain(` [0] "${"y".repeat(399)}…`); expect(described).not.toContain("y".repeat(400)); }); }); describe("waitForCondition()", () => { it("reports the predicate, its arguments, and the page it ran out against", async () => { await load("/booted-shell"); const message = await shortNetRejectionMessage(() => waitForCondition(page, (_probe, slug: string) => slug === "never", { args: ["members-7"], }) ); expect(message).toContain( "waitForCondition did not resolve within 1500ms.", ); expect(message).toContain("awaited condition:"); expect(message).toContain(' [0] "members-7"'); expect(message).toContain("did:key:zBootedShellFixture"); expect(message).not.toContain("predicate threw:"); }); it("names the throw a predicate made, which the page cannot show", async () => { await load("/booted-shell"); const message = await shortNetRejectionMessage(() => waitForCondition(page, () => { throw new Error("the predicate itself is broken"); }) ); expect(message).toContain( "predicate threw: Error: the predicate itself is broken", ); }); }); describe("describeShellPage()", () => { it("returns a block naming the view and identity the page holds", async () => { await load("/booted-shell"); const described = describeShellPage(await readShellPageProbe(page)); expect(described).toContain( 'globalThis.app: present, holding view {"builtin":"home"} and ' + "did:key:zBootedShellFixture", ); expect(described).toContain("x-root-view: present"); }); it("omits the document's text for a page that is the shell", async () => { await load("/shell"); const described = describeShellPage(await readShellPageProbe(page)); expect(described).not.toContain("document text:"); }); // The four pending-request cases render a probe built here rather than one // read from a page. `describeShellPage` is a function of that record // alone, and the four lines it chooses between differ by a few words, so // each case asserts a whole line: a check for "pending runtime requests: // none" is satisfied by three of the four. it("names each request the runtime is waiting on, and its age", () => { const lines = describedLines({ pendingRequests: [ { msgId: 7, type: RequestType.Dispose, ageMs: 41203 }, { msgId: 9, type: RequestType.CellSubscribe, ageMs: 84 }, ], }); expect(lines).toContain(" pending runtime requests (2, oldest first):"); expect(lines).toContain(" dispose (msgId 7), outstanding for 41203ms"); expect(lines).toContain( " cell:subscribe (msgId 9), outstanding for 84ms", ); }); it("reports that a runtime with nothing in flight has none", () => { expect(describedLines({ pendingRequests: [] })).toContain( " pending runtime requests: none", ); }); it("reports that a page carrying no runtime has none", () => { expect(describedLines({ runtime: false })).toContain( " pending runtime requests: none, the page carries no runtime", ); }); it("reports a runtime that does not report its requests", () => { expect(describedLines({})).toContain( " pending runtime requests: this runtime does not report them", ); }); it("reports why the requests could not be read", () => { expect( describedLines({ pendingRequestsError: "Error: the connection is gone", }), ).toContain( " pending runtime requests: reading them threw: " + "Error: the connection is gone", ); }); describe("worker warnings and errors", () => { // These render a probe built here, as the pending-request cases do, and // assert a whole line for the same reason: the lines they choose between // differ by a few words. it("names each warning or error the worker logged, and its counts", () => { const lines = describedLines({ workerProblems: [ { logger: "storage.v2", message: "sync-load-failure", warn: 1, error: 3, }, { logger: "scheduler", message: "schedule-error", warn: 0, error: 2, }, ], }); expect(lines).toContain( " worker warnings and errors (2 kinds, most errors first):", ); expect(lines).toContain( " storage.v2 sync-load-failure: 3 error, 1 warn", ); expect(lines).toContain( " scheduler schedule-error: 2 error, 0 warn", ); }); it("lists twenty kinds of warning and error and counts the rest", () => { const lines = describedLines({ workerProblems: Array.from({ length: 23 }, (_, index) => ({ logger: "runner", message: `problem-${index}`, warn: 0, error: 23 - index, })), }); expect(lines).toContain(" runner problem-19: 4 error, 0 warn"); expect(lines).not.toContain(" runner problem-20: 3 error, 0 warn"); expect(lines).toContain(" and 3 more"); }); it("names a single kind of warning or error without a plural", () => { expect( describedLines({ workerProblems: [ { logger: "runner", message: "action-failed", warn: 1, error: 0 }, ], }), ).toContain( " worker warnings and errors (1 kind, most errors first):", ); }); it("reports that a worker with no warning or error logged has none", () => { expect(describedLines({ workerProblems: [] })).toContain( " worker warnings and errors: none", ); }); it("reports that a page carrying no runtime has no worker to ask", () => { expect(describedLines({ runtime: false })).toContain( " worker warnings and errors: none, the page carries no runtime", ); }); it("reports a runtime that does not report its worker's logs", () => { expect(describedLines({})).toContain( " worker warnings and errors: this runtime does not report them", ); }); it("reports why the worker's logs could not be read", () => { expect( describedLines({ workerProblemsError: "Error: the worker did not answer within 30000ms", }), ).toContain( " worker warnings and errors: reading them failed: " + "Error: the worker did not answer within 30000ms", ); }); }); }); describe("describeStateWaitFailure()", () => { it("returns a block naming the awaited view beside the view held", async () => { await load("/booted-shell"); const described = await describeStateWaitFailure( page, { view: { spaceName: "some-space" } }, undefined, ); expect(described).toContain('awaited view: {"spaceName":"some-space"}'); expect(described).toContain( "last state read: none (the page never yielded a state)", ); expect(described).toContain( 'globalThis.app: present, holding view {"builtin":"home"}', ); }); it("names both identity DIDs when an identity was awaited", async () => { await load("/booted-shell"); const awaited = await Identity.generate(); const held = await Identity.generate(); const described = await describeStateWaitFailure( page, { view: { builtin: "home" }, identity: awaited }, { view: { builtin: "home" }, identityDid: held.did(), apiUrl: origin, config: {}, }, ); expect(described).toContain(`awaited identity: ${awaited.did()}`); expect(described).toContain( `last state read: view {"builtin":"home"}, identity ${held.did()}`, ); }); it("names a document that is not the shell", async () => { await load("/proxy-failure"); const described = await describeStateWaitFailure( page, { view: { builtin: "home" } }, undefined, ); expect(described).toContain("x-root-view: absent"); expect(described).toContain("globalThis.app: absent"); expect(described).toContain("response status: 502"); expect(described).toContain("document text: Failed to proxy to "); }); }); describe("readAndDescribeShellPage()", () => { it("returns the same block describeShellPage() renders", async () => { await load("/booted-shell"); expect(await readAndDescribeShellPage(page)).toBe( describeShellPage(await readShellPageProbe(page)), ); }); it("returns a reason rather than waiting out a page that never answers", async () => { // Its own browser, because the wedge below is permanent: the probe runs // in the page, and a page whose main thread never yields never answers // one. A report written for that state must still arrive, or the // failure it was describing never reaches the test runner at all. const wedged = await Browser.launch(); try { const wedgedPage = await wedged.newPage(`${origin}/shell`); wedgedPage.evaluate(() => { while (true) { /* hold the main thread */ } }).catch(() => {}); const described = await readAndDescribeShellPage(wedgedPage); expect(described).toContain("the page could not be probed:"); expect(described).toContain("did not answer within"); } finally { await wedged.close(); } }); it("returns the reason when the page cannot be read", async () => { const closed = await browser.newPage(); await closed.close(); const described = await readAndDescribeShellPage(closed); expect(described).toContain("the page could not be probed:"); }); }); describe("disposePageRuntime()", () => { // The teardown reports through `console.warn` rather than throwing, so // each case reads what was warned. Collecting every warning and matching // on the text keeps a case from turning on how many other warnings the // browser produced. it("warns naming the requests the runtime was still waiting on", async () => { await load("/unresponsive-runtime"); const warnings: string[] = []; using _warn = stub(console, "warn", (...args: unknown[]) => { warnings.push(args.join(" ")); }); await disposePageRuntime(page); const warned = warnings.join("\n"); expect(warned).toContain( "Disposing the shell page runtime failed: Error: the worker stopped " + "answering", ); expect(warned).toContain("pending runtime requests (2, oldest first):"); expect(warned).toContain("dispose (msgId 7), outstanding for 41203ms"); }); it("drops a runtime that answers, and warns nothing", async () => { await load("/idle-runtime"); const warnings: string[] = []; using _warn = stub(console, "warn", (...args: unknown[]) => { warnings.push(args.join(" ")); }); await disposePageRuntime(page); expect(warnings.join("\n")).not.toContain( "Disposing the shell page runtime failed", ); expect( await page.evaluate(() => globalThis.commonfabric.rt === undefined), ).toBe(true); }); }); describe("login()", () => { it("throws naming the identity and the page it failed against", async () => { await load("/refusing-shell"); const identity = await Identity.generate({ implementation: "noble" }); const message = await rejectionMessage(login(page, identity)); expect(message).toContain( `Logging in as ${identity.did()} failed: Error: the key store is not open`, ); expect(message).toContain("x-root-view: present"); expect(message).toContain( 'globalThis.app: present, holding view {"builtin":"home"}', ); }); }); });