/** * Fences, drawn. * * Colour is the point of all of this and there is no colour without a browser: * the highlighter is loaded on demand, tokenises in the page, and paints into a * shadow root that only a real DOM has. What can be decided without one — what * a fence's language and title mean, and what to do about a patch nobody wrote * correctly — is `code.test.ts`, and none of it is repeated here. * * Playwright's selectors cross an open shadow root, so the renderer's own * attributes are the address for what it drew: `data-file` is a file, and * `data-diff` is a change. Which of the two turned up is most of what these * tests are about. */ import { content, expect, test, written } from "./room"; import { expectNoHorizontalOverflow } from "./responsive"; import type { Locator, Page } from "@playwright/test"; let MENU = { name: "Insert block" }; /** The language control is a button that opens a listbox. */ async function chooseLanguage(scope: Locator, from: string, to: string) { await scope.getByRole("button", { name: `Code language: ${from}` }).click(); await scope.page().getByRole("option", { name: to, exact: true }).click(); } /** A fence with the counts a person or a model actually writes. */ const PATCH = `\`\`\`diff --- a/apps/server/src/plan/room.ts +++ b/apps/server/src/plan/room.ts @@ -1,9 +1,9 @@ export function open(room: string): Plan { - return { doc, epoch: 1 }; + let epoch = rotate(room); + return { doc, epoch }; } \`\`\` `; const WIDE_MERMAID = `flowchart LR A[Compact document measure that readers can track] --> B[Independent widget scrollers that preserve the document width] B --> C[Keyboard-safe destination switching across responsive workspaces] C --> D[Durable agent change chips and collaborator cursor labels]`; const TALL_MERMAID = WIDE_MERMAID.replace("flowchart LR", "flowchart TD"); /** How many colours the highlighter ended up using. */ async function colours(page: Page): Promise { return await page .locator("[data-line] span[style*='color']") .evaluateAll(nodes => new Set(nodes.map(node => (node as HTMLElement).style.color)).size); } async function diagramGeometry(page: Page) { let region = content(page).getByRole("region", { name: "Diagram preview" }); return await region.evaluate(element => { let svg = element.querySelector("svg")!; let bounds = svg.getBoundingClientRect(); let labels = [...svg.querySelectorAll(".node .nodeLabel")]; let drawn = [...svg.querySelectorAll("foreignObject, rect, path[marker-end]")]; return { edges: svg.querySelectorAll("path[marker-end]").length, labels: labels.length, nodes: svg.querySelectorAll("rect").length, region: { clientWidth: element.clientWidth, scrollWidth: element.scrollWidth }, regionHeight: element.getBoundingClientRect().height, svg: { height: bounds.height, width: bounds.width }, verticallyContained: drawn.every(item => { let rectangle = item.getBoundingClientRect(); return rectangle.top >= bounds.top - 1 && rectangle.bottom <= bounds.bottom + 1; }), }; }); } test("a named fence is coloured with its source hidden by default", async ({ join, seed }) => { await seed("```ts\nexport function open(room: string) {\n\treturn 1;\n}\n```\n"); let page = await join("ana"); await expect(content(page).locator("[data-file]")).toBeVisible(); // More than one colour is the whole claim: one would mean the grammar // never loaded and every token was painted as plain text. await expect.poll(() => colours(page)).toBeGreaterThan(1); // A preview replaces the source until this reader asks to edit it. The // document still carries the source, but it does not compete with the view. await expect(content(page).locator("[data-plan-source]")).toBeHidden(); await expect(content(page).getByRole("button", { name: "Show source" })).toBeVisible(); }); test("a wide preview scrolls inside its code widget", async ({ join, seed }) => { await seed( '```ts\nexport const unbrokenPreviewLine = "ThisPreviewLineIsDeliberatelyLongEnoughToRequireTheCodeWidgetToOwnHorizontalScrollingWithoutWideningTheCollaborativeDocument";\n```\n', ); let page = await join("ana", { viewport: { width: 390, height: 844 } }); let preview = content(page).locator("[data-plan-preview]"); let rendered = preview.locator(":scope > div"); await expect(preview).toBeVisible(); expect(await rendered.evaluate(node => node.scrollWidth > node.clientWidth)).toBe(true); await expectNoHorizontalOverflow(page); }); test("an inserted wide diagram stays readable in its own keyboard scroller", async ({ join }) => { let page = await join("ana", { viewport: { width: 1440, height: 1000 } }); await content(page).click(); await page.keyboard.type("/diagram"); await page.getByRole("listbox", MENU).getByRole("option", { name: "Diagram" }).click(); await page.keyboard.insertText(WIDE_MERMAID); let region = content(page).getByRole("region", { name: "Diagram preview" }); await expect(region).toBeVisible(); for (let viewport of [{ width: 1440, height: 1000 }, { width: 390, height: 844 }]) { await page.setViewportSize(viewport); let geometry = await diagramGeometry(page); expect(geometry.labels).toBe(4); expect(geometry.nodes).toBeGreaterThanOrEqual(4); expect(geometry.edges).toBe(3); expect(geometry.verticallyContained).toBe(true); expect(geometry.region.scrollWidth).toBeGreaterThan(geometry.region.clientWidth); await expectNoHorizontalOverflow(page); } await region.focus(); expect(await region.evaluate(node => node === document.activeElement)).toBe(true); await page.keyboard.press("ArrowRight"); await expect.poll(() => region.evaluate(node => node.scrollLeft)).toBeGreaterThan(0); }); test("a diagram recomputes its height after source and viewport changes", async ({ join, seed }) => { await seed(`\`\`\`mermaid ${WIDE_MERMAID} \`\`\` `); let page = await join("ana", { viewport: { width: 1440, height: 1000 } }); let region = content(page).getByRole("region", { name: "Diagram preview" }); await expect(region).toBeVisible(); let wide = await diagramGeometry(page); await content(page).getByRole("button", { name: "Show source" }).click(); let source = content(page).locator("[data-plan-source]"); await source.selectText(); await page.keyboard.insertText(TALL_MERMAID); await expect.poll(async () => (await diagramGeometry(page)).svg.height).toBeGreaterThan( wide.svg.height * 2, ); let rerendered = await diagramGeometry(page); expect(rerendered.regionHeight).toBeGreaterThanOrEqual(rerendered.svg.height); await page.setViewportSize({ width: 390, height: 844 }); await expect.poll(async () => (await diagramGeometry(page)).region.clientWidth).toBeLessThan( rerendered.region.clientWidth, ); let resized = await diagramGeometry(page); expect(resized.svg.height).toBeCloseTo(rerendered.svg.height, 0); expect(resized.regionHeight).toBeGreaterThanOrEqual(resized.svg.height); expect(resized.verticallyContained).toBe(true); await expectNoHorizontalOverflow(page); }); test("a diagram is shown with its source hidden by default", async ({ join, seed }) => { await seed("```mermaid\ngraph TD;\nA-->B;\n```\n"); let page = await join("ana"); await expect(content(page).locator("[data-plan-preview] svg")).toBeVisible(); await expect(content(page).locator("[data-plan-source]")).toBeHidden(); await expect(content(page).getByRole("button", { name: "Show source" })).toBeVisible(); }); test("diagram labels keep arrows, operators and entity codes as written", async ({ join, seed }) => { await seed(`\`\`\`mermaid flowchart LR A["create_router(pool: SqlitePool, config: Config) -> Router"] -- "a && b => c" --> B["Vec#lt;T#gt; or Option#lt;&str#gt;"] B --> C["say #quot;hi#quot; when n #gt; 0"] \`\`\` `); let page = await join("ana"); let svg = content(page).locator("[data-plan-preview] svg"); await expect(svg).toBeVisible(); let labels = await svg.evaluate(element => [...element.querySelectorAll("g.label, g.edgeLabel")].map(label => (label.textContent ?? "").replace(/\s+/g, " ").trim() ) ); expect(labels).toContain("create_router(pool: SqlitePool, config: Config) -> Router"); expect(labels).toContain("a && b => c"); expect(labels).toContain("Vec or Option<&str>"); expect(labels).toContain('say "hi" when n > 0'); expect(labels.join(" ")).not.toMatch(/&(?:gt|lt|amp|quot|#\d+);|#(?:gt|lt|quot);/); }); test("an invalid diagram leaves its error inside the fence", async ({ join, seed }) => { await seed("```mermaid\nflowchart LR\nA[raw MemEntry[]]\n```\n"); let page = await join("ana"); let error = content(page).locator("[data-plan-error]"); await expect(error).toBeVisible(); await expect(error.getByText("This diagram could not be drawn")).toBeVisible(); await expect(error.locator(".plan-error-message")).not.toBeEmpty(); await expect( page.locator("body > div").filter({ hasText: "Syntax error in text" }), ).toHaveCount(0); }); test("naming a fence colours it, and the name reaches the file", async ({ join, room, seed }) => { await seed("```\nlet total = 1;\n```\n"); let page = await join("ana"); // Nothing is drawn for a fence with no language: an uncoloured copy beside // an uncoloured original is two of the same thing. await expect(content(page).locator("[data-file]")).toHaveCount(0); await chooseLanguage(content(page), "Plain text", "TypeScript"); await expect(content(page).locator("[data-file]")).toBeVisible(); await expect.poll(() => colours(page)).toBeGreaterThan(1); // The language is a property of the fence, so choosing one is an edit — // and an edit that does not reach the file is a colour nobody else sees. await written(page, room, /^```typescript$/m); }); test("a patch is drawn as the change it describes", async ({ join, seed }) => { await seed(PATCH); let page = await join("ana"); let diff = content(page).locator("[data-diff]"); await expect(diff).toBeVisible(); // Named, because the filename is what a patch has that a snippet does not, // and it survives the `a/` and `b/` git puts in front of it. await expect(content(page).locator("[data-title]")).toHaveText("apps/server/src/plan/room.ts"); await expect(diff.locator("[data-line-type='change-addition']").first()).toBeVisible(); await expect(diff.locator("[data-line-type='change-deletion']").first()).toBeVisible(); // All of it, because the header claimed nine lines where there are four. A // renderer that believed the header stops reading when it runs out, and // draws half a change — which is a change nobody proposed. await expect(diff).toContainText("let epoch = rotate(room);"); await expect(diff).toContainText("return { doc, epoch };"); await expect(diff).toContainText("return { doc, epoch: 1 };"); }); test("a fence that is not a patch is drawn as the text it is", async ({ join, seed }) => { await seed("```diff\n- let a = 1;\n+ let a = 2;\n```\n"); let page = await join("ana"); // Coloured as a diff, which it looks like, and not presented as a change // to a file: there is no file, no line numbers and no hunk here, and // inventing them would put all three in the reader's head. await expect(content(page).locator("[data-file]")).toBeVisible(); await expect(content(page).locator("[data-diff]")).toHaveCount(0); }); test("an invalid diff keeps its authored filename", async ({ join, seed }) => { await seed('```diff title="broken.patch"\nnot a patch\n```\n'); let page = await join("ana"); await expect(content(page).locator("[data-file]")).toBeVisible(); await expect(content(page).getByText("broken.patch", { exact: true })).toBeVisible(); await expect(content(page).locator("[data-diff]")).toHaveCount(0); }); test("enter is a newline in a fence, and twice over is the way out", async ({ join, room }) => { let page = await join("ana"); await content(page).click(); await page.keyboard.type("/code"); await page.getByRole("listbox", MENU).getByRole("option", { name: "Code block" }).click(); await page.keyboard.type("let a = 1;"); await page.keyboard.press("Enter"); await page.keyboard.type("let b = 2;"); // Lexical asks a block to make its own successor and a code block cannot, // so without this Enter does nothing at all — and a fence at the end of a // plan is a corner somebody can be typed into. await page.keyboard.press("Enter"); await page.keyboard.press("Enter"); await page.keyboard.type("And then ship it."); await written(page, room, /```\nlet a = 1;\nlet b = 2;\n```\n\nAnd then ship it\./); }); test("hiding the source leaves what was drawn from it", async ({ join, seed }) => { await seed("```ts\nexport function open(room: string) {\n\treturn 1;\n}\n```\n"); let page = await join("ana"); await expect(content(page).locator("[data-file]")).toBeVisible(); await content(page).getByRole("button", { name: "Show source" }).click(); await expect(content(page).locator("[data-plan-source]")).toBeVisible(); await content(page).getByRole("button", { name: "Hide source" }).click(); await expect(content(page).locator("[data-plan-source]")).toBeHidden(); await expect(content(page).locator("[data-file]")).toBeVisible(); await expect(content(page).getByRole("button", { name: "Show source" })).toBeVisible(); }); /* * Getting into a rendered block. * * The preview is derived and cannot take a caret, so every way of arriving at * one has to end with the caret in the source, or with focus on the preview * and a key that leads into the source from there. Typing is how each test * proves where the caret went: the file is the only witness that cannot be * fooled by a caret drawn somewhere it is not. */ test("clicking drawn code opens its source at the clicked line", async ({ join, room, seed }) => { await seed("```ts\nlet a = 1;\nlet b = 2;\n```\n"); let page = await join("ana"); let block = content(page).locator(".planCode"); await block.locator("[data-line='2']").click(); // One copy: the source replaces the coloured lines rather than joining them. await expect(block.locator("[data-plan-source]")).toBeVisible(); await expect(block.locator("[data-file]")).toBeHidden(); await expect(content(page)).toBeFocused(); await page.keyboard.type("X"); await written(page, room, /^let a = 1;\n[^\n]*X[^\n]*\n```/m); await page.keyboard.press("Escape"); await expect(block.locator("[data-plan-source]")).toBeHidden(); await expect(block.getByRole("group", { name: "Code preview" })).toBeFocused(); }); test("clicking a drawn diff line opens the patch at that line", async ({ join, room, seed }) => { await seed(PATCH); let page = await join("ana"); await content(page).locator("[data-line][data-line-type='change-addition']").first().click(); await page.keyboard.type("X"); await written(page, room, /^\+(?=[^\n]*rotate)[^\n]*X[^\n]*$/m); }); test("a focused preview opens on Enter, Space or typing, and never scrolls", async ({ join, room, seed }) => { await seed("```ts\nlet a = 1;\n```\n"); let page = await join("ana"); let block = content(page).locator(".planCode"); let preview = block.getByRole("group", { name: "Code preview" }); let scroller = page.locator("[data-plan-scroll]"); await preview.focus(); let before = await scroller.evaluate(node => node.scrollTop); await page.keyboard.press("Space"); await expect(block.locator("[data-plan-source]")).toBeVisible(); expect(await scroller.evaluate(node => node.scrollTop)).toBe(before); await page.keyboard.press("Escape"); await expect(preview).toBeFocused(); await page.keyboard.press("Enter"); await expect(content(page)).toBeFocused(); await page.keyboard.press("Escape"); await expect(preview).toBeFocused(); await page.keyboard.type("Z"); await written(page, room, /^let a = 1;Z$/m); }); test("arrows step into code, onto a diagram, and out again", async ({ join, room, seed }) => { await seed("Before\n\n```ts\nlet a = 1;\n```\n\n```mermaid\ngraph TD;\nA-->B;\n```\n\nAfter\n"); let page = await join("ana"); let [code, diagram] = [ content(page).locator(".planCode").nth(0), content(page).locator(".planCode").nth(1), ]; let region = diagram.getByRole("region", { name: "Diagram preview" }); await expect(region).toBeVisible(); await expect(code.locator("[data-file]")).toBeVisible(); // A hidden source has no layout, so the browser alone would skip both. await content(page).getByText("Before").click(); await page.keyboard.press("ArrowDown"); await expect(code.locator("[data-plan-source]")).toBeVisible(); await page.keyboard.type("Q"); await written(page, room, /^Qlet a = 1;$/m); // A drawing is held as a block, not opened: arrowing past it is reading. await page.keyboard.press("ArrowDown"); await expect(region).toBeFocused(); await expect(code.locator("[data-plan-source]")).toBeHidden(); await expect(diagram.locator("[data-plan-source]")).toBeHidden(); // Enter edits it, with the drawing kept above; Escape comes back. await page.keyboard.press("Enter"); await expect(diagram.locator("[data-plan-source]")).toBeVisible(); await expect(region).toBeVisible(); await page.keyboard.press("Escape"); await expect(region).toBeFocused(); await expect(diagram.locator("[data-plan-source]")).toBeHidden(); await page.keyboard.press("ArrowDown"); await page.keyboard.type("!"); await written(page, room, /^!After$/m); await page.keyboard.press("ArrowUp"); await expect(region).toBeFocused(); await page.keyboard.press("ArrowUp"); await page.keyboard.type("R"); await written(page, room, /^RQlet a = 1;$/m); }); test("dragging across drawn code selects it to copy, without opening", async ({ join, seed }) => { await seed("```ts\nlet a = 1;\nexport function open(room: string) {}\n```\n"); let page = await join("ana"); let block = content(page).locator(".planCode"); let line = block.locator("[data-line='2']"); await expect(line).toBeVisible(); await page.evaluate(() => window.addEventListener("copy", event => { (window as unknown as { copied: unknown }).copied = { prevented: event.defaultPrevented, text: document.getSelection()?.toString(), }; }) ); let box = (await line.boundingBox())!; await page.mouse.move(box.x + 16, box.y + box.height / 2); await page.mouse.down(); await page.mouse.move(box.x + 120, box.y + box.height / 2, { steps: 8 }); await page.mouse.up(); let dragged = await page.evaluate(() => document.getSelection()?.toString() ?? ""); // Shift extends it from where the drag began, as it would in text. await page.keyboard.down("Shift"); await page.mouse.click(box.x + 200, box.y + box.height / 2); await page.keyboard.up("Shift"); await expect(block.locator("[data-plan-source]")).toBeHidden(); await expect(block.locator("[data-file]")).toBeVisible(); let selected = await page.evaluate(() => document.getSelection()?.toString() ?? ""); expect(selected.length).toBeGreaterThan(dragged.length); expect(selected.startsWith(dragged)).toBe(true); expect("export function open(room: string) {}").toContain(selected); // The browser copies what is selected; Lexical must not swap in its own. await page.keyboard.press("ControlOrMeta+c"); await expect .poll(() => page.evaluate(() => (window as unknown as { copied: unknown }).copied)) .toEqual({ prevented: false, text: selected }); }); test("arrows reach code inside a callout and a list item", async ({ join, room, seed }) => { await seed( '\n\nInside.\n\n```ts\nlet a = 1;\n```\n\n\n\n- Item\n\n ```ts\n let b = 2;\n ```\n\n- Next\n', ); let page = await join("ana"); await expect(content(page).locator("[data-file]")).toHaveCount(2); await content(page).getByText("Inside.").click(); await page.keyboard.press("ArrowDown"); await page.keyboard.type("Q"); await written(page, room, /^\s*Qlet a = 1;$/m); await content(page).getByText("Next").click(); await page.keyboard.press("ArrowUp"); await page.keyboard.type("R"); await written(page, room, /^\s*Rlet b = 2;$/m); }); /* * Two people in one fence. * * What is drawn is a projection of the shared source, so the thing worth * asserting is which of these facts travels: the text does, the language does, * and hiding the source does not. Everything drawn follows from the first two * and belongs to nobody. * * Playwright drives one page at a time, so these are edits that interleave * rather than collide — the same as everything in `collab.e2e.ts`, and enough * to tell a shared text apart from a copy each renderer owns. A merge of two * genuinely simultaneous keystrokes is `apps/web/src/collab.test.ts`, which * runs two providers against a real server and can hold one of them back. */ test("two people typing in one fence both end up in it", async ({ join, room, seed }) => { await seed("```ts\nlet a = 1;\nlet b = 2;\n```\n"); let ana = await join("ana"); let bo = await join("bo"); /* * Select everything and collapse the selection to the end you want. * Clicking a line means clicking a coordinate inside a `
`, and
	 * Home and End are a different key on each platform; this is neither.
	 * The fence is the whole document here, so its ends are the document's.
	 */
	await content(ana).getByRole("button", { name: "Show source" }).click();
	await content(ana).locator("[data-plan-source]").click();
	await ana.keyboard.press("ControlOrMeta+a");
	await ana.keyboard.press("ArrowLeft");
	await ana.keyboard.type("// ana ");

	await content(bo).getByRole("button", { name: "Show source" }).click();
	await content(bo).locator("[data-plan-source]").click();
	await bo.keyboard.press("ControlOrMeta+a");
	await bo.keyboard.press("ArrowRight");
	await bo.keyboard.type(" // bo");

	for (let page of [ana, bo]) {
		// The source is one shared text, so neither edit replaces the other.
		// A renderer holding a copy of the fence would have overwritten
		// whichever arrived first.
		await expect(content(page).locator("[data-plan-source]")).toContainText("// ana let a = 1;");
		await expect(content(page).locator("[data-plan-source]")).toContainText("let b = 2; // bo");

		// And what is drawn is drawn from that, on each page separately.
		await expect(content(page).locator("[data-file]")).toContainText("// ana let a = 1;");
		await expect(content(page).locator("[data-file]")).toContainText("let b = 2; // bo");
	}

	await written(ana, room, /^\/\/ ana let a = 1;\nlet b = 2; \/\/ bo$/m);
});

test("a language chosen by one is a change for everyone", async ({ join, room, seed }) => {
	await seed("```\nlet total = 1;\n```\n");
	let ana = await join("ana");
	let bo = await join("bo");

	await expect(content(bo).locator("[data-file]")).toHaveCount(0);

	await chooseLanguage(content(ana), "Plain text", "TypeScript");

	// The language is a property of the fence rather than a way of looking at
	// it, so it travels: the other reader's copy is coloured too, and their
	// control says what it now is.
	await expect(content(bo).getByRole("button", { name: "Code language: TypeScript" }))
		.toBeVisible();
	await expect(content(bo).locator("[data-file]")).toBeVisible();
	await expect.poll(() => colours(bo)).toBeGreaterThan(1);

	await written(ana, room, /^```typescript$/m);
});

test("showing the source leaves everybody else's hidden", async ({ join, seed }) => {
	await seed("```ts\nlet a = 1;\n```\n");
	let ana = await join("ana");
	let bo = await join("bo");

	await expect(content(bo).locator("[data-file]")).toBeVisible();
	await content(ana).getByRole("button", { name: "Show source" }).click();
	await expect(content(ana).locator("[data-plan-source]")).toBeVisible();

	// Visibility is a way of looking at a block, not a fact about it. A reader
	// who reveals a fence for themselves has not revealed it for the other
	// person, who should keep reading the preview.
	await expect(content(bo).locator("[data-plan-source]")).toBeHidden();
	await expect(content(bo).getByRole("button", { name: "Show source" })).toBeVisible();
});

test("the language menu is a keyboard-operable listbox", async ({ join, room, seed }) => {
	await seed("```typescript\nlet total = 1;\n```\n");
	let page = await join("ana");
	let trigger = content(page).getByRole("button", { name: "Code language: TypeScript" });
	let list = page.getByRole("listbox", { name: "Code language" });

	await trigger.focus();
	await page.keyboard.press("ArrowDown");
	await expect(list).toBeVisible();

	await page.keyboard.press("Escape");
	await expect(list).toBeHidden();
	await expect(trigger).toBeFocused();

	await trigger.click();
	await expect(list).toBeVisible();
	await page.mouse.click(5, 5);
	await expect(list).toBeHidden();

	// Tab from the open menu continues from the trigger, not from the end of the page.
	// The source toggle sits just before the trigger, so Shift+Tab lands on it.
	await trigger.focus();
	await page.keyboard.press("ArrowDown");
	await expect(list).toBeVisible();
	await page.keyboard.press("Shift+Tab");
	await expect(list).toBeHidden();
	await expect(content(page).getByRole("button", { name: "Show source" })).toBeFocused();

	await trigger.focus();
	await page.keyboard.press("ArrowDown");
	await page.keyboard.press("ArrowDown");
	await page.keyboard.press("Enter");
	await expect(list).toBeHidden();
	await expect(content(page).getByRole("button", { name: /^Code language: (?!TypeScript)/ }))
		.toBeVisible();
	await written(page, room, /^```(?!typescript$)\S+$/m);
});

test("the language menu takes focus before the next animation frame", async ({ join, page, seed }) => {
	await seed("```typescript\nlet total = 1;\n```\n");
	let time = new Date("2026-01-01T00:00:00Z");
	await page.clock.install({ time });
	await join("ana");
	let trigger = content(page).getByRole("button", { name: "Code language: TypeScript" });
	let list = page.getByRole("listbox", { name: "Code language" });

	// Pause ahead of the fixed epoch rather than racing two system clocks.
	await page.clock.pauseAt(new Date(time.getTime() + 60_000));
	await trigger.focus();
	await page.keyboard.press("ArrowDown");
	await expect(list).toBeFocused();
	await page.keyboard.press("ArrowDown");
	await page.keyboard.press("Enter");
	await page.clock.resume();
	await expect(content(page).getByRole("button", { name: "Code language: XML", exact: true }))
		.toBeVisible();
});

test("the language menu closes on Escape from its trigger and when focus leaves", async ({ join, seed }) => {
	await seed("```typescript\nlet total = 1;\n```\n");
	let page = await join("ana");
	let trigger = content(page).getByRole("button", { name: "Code language: TypeScript" });
	let list = page.getByRole("listbox", { name: "Code language" });

	await trigger.click();
	await expect(list).toBeVisible();
	await trigger.focus();
	await page.keyboard.press("Escape");
	await expect(list).toBeHidden();
	await expect(trigger).toBeFocused();

	await trigger.click();
	await expect(list).toBeVisible();
	await content(page).getByRole("button", { name: "Show source" }).focus();
	await expect(list).toBeHidden();
});