/** * The two things that appear because of where the caret is. * * Both are placed by measurement against a live rectangle, so neither can be * covered by `bun test` — `bubble.test.ts` and `slash.test.ts` cover the * decisions those measurements feed, and stop at the point where a browser is * required. Everything here is on the other side of that line. */ import { content, expect, test, written } from "./room"; import type { Page } from "@playwright/test"; let MENU = { name: "Insert block" }; let BUBBLE = { name: "Text formatting" }; async function insertCallout(page: Page) { await content(page).click(); await page.keyboard.type("/callout"); await page.getByRole("listbox", MENU).getByRole("option", { name: "Callout" }).click(); return content(page).locator("aside[data-plan-type]"); } test("filtering resets the option Enter will choose", async ({ join }) => { let page = await join("ana"); await content(page).click(); await page.keyboard.type("/"); let menu = page.getByRole("listbox", MENU); await page.keyboard.press("ArrowDown"); await page.keyboard.press("ArrowDown"); await page.keyboard.press("ArrowDown"); await page.keyboard.type("call"); await expect(menu.getByRole("option", { selected: true })).toHaveText("Callout"); await page.keyboard.press("Enter"); await expect(page.getByRole("combobox", { name: "Change callout type: Note" })).toBeVisible(); }); test("a callout edits like prose and keeps its type controls out of the way", async ({ join, room }) => { let page = await join("ana"); let callout = await insertCallout(page); await expect(callout).toHaveAttribute("data-plan-type", "note"); let title = callout.getByRole("textbox", { name: "Callout title" }); let trigger = callout.getByRole("combobox", { name: "Change callout type: Note" }); await expect(page.getByRole("listbox", { name: "Callout type" })).toHaveCount(0); await title.fill("Worth knowing"); await trigger.click(); let types = page.getByRole("listbox", { name: "Callout type" }); await expect(types).toBeVisible(); await types.getByRole("option", { name: "Warning" }).click(); await expect(callout).toHaveAttribute("data-plan-type", "warning"); await expect(title).toHaveText("Worth knowing"); await written(page, room, /type="warning" title="Worth knowing"/); }); test("escape closes the menu and leaves what was typed", async ({ join }) => { let page = await join("ana"); await content(page).click(); await page.keyboard.type("/tab"); await expect(page.getByRole("listbox", MENU)).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByRole("listbox", MENU)).toHaveCount(0); await expect(content(page)).toContainText("/tab"); }); test("choosing a table inserts one, with a header", async ({ join }) => { let page = await join("ana"); await content(page).click(); await page.keyboard.type("/table"); await page.getByRole("listbox", MENU).getByRole("option", { name: "Table" }).click(); await expect(content(page).locator("table")).toHaveCount(1); await expect(content(page).locator("th")).toHaveCount(3); await expect(content(page).locator("tr")).toHaveCount(3); // The query that summoned it has to go: leaving "/table" above the table // is the menu writing its own invocation into the document. await expect(content(page)).not.toContainText("/table"); }); test("a selection raises exactly one toolbar", async ({ join }) => { let page = await join("ana"); await content(page).click(); await page.keyboard.type("Pick a storage format."); await page.keyboard.press("Shift+Home"); // Two toolbars is the ported defect: one from the plugin list and one from // the surface, stacked, so the top one takes every click and the other is // only visible as a shadow slightly out of register. await expect(page.getByRole("toolbar", BUBBLE)).toHaveCount(1); }); test("a mark from the toolbar reaches the file", async ({ join, room }) => { let page = await join("ana"); await content(page).click(); await page.keyboard.type("Pick a storage format."); await page.keyboard.press("Shift+Home"); let bubble = page.getByRole("toolbar", BUBBLE); await expect(bubble.getByRole("button", { name: "Bold" })).toHaveAttribute( "aria-pressed", "false", ); await bubble.getByRole("button", { name: "Bold" }).click(); await expect(bubble.getByRole("button", { name: "Bold" })).toHaveAttribute( "aria-pressed", "true", ); await written(page, room, /^\*\*Pick a storage format\.\*\*$/m); }); test("the block type menu converts the block it names", async ({ join, room }) => { let page = await join("ana"); await content(page).click(); await page.keyboard.type("What we are building"); await page.keyboard.press("Shift+Home"); let bubble = page.getByRole("toolbar", BUBBLE); await bubble.getByRole("button", { name: "Block type: Text" }).click(); // The submenu replaces the toolbar's contents rather than opening beside // it, so there is never a second popup to decide between. await expect(bubble.getByRole("button", { name: "Bold" })).toHaveCount(0); await bubble.getByRole("button", { name: "Heading 2" }).click(); await expect(content(page).getByRole("heading", { level: 2 })).toHaveText( "What we are building", ); await written(page, room, /^## What we are building$/m); }); test("the link prompt refuses a scheme the dialect will not carry", async ({ join, room }) => { let page = await join("ana"); await content(page).click(); await page.keyboard.type("Read the docs."); await page.keyboard.press("Shift+Home"); let said: string[] = []; page.on("dialog", async dialog => { said.push(dialog.message()); await (dialog.type() === "prompt" ? dialog.accept("javascript:alert(1)") : dialog.dismiss()); }); await page.getByRole("toolbar", BUBBLE).getByRole("button", { name: "Add link" }).click(); /* * Refused where the button is, not where the document is. A URL the * dialect rejects applies locally, syncs, and is then refused by the * server — which cannot undo a Yjs transaction, so it rebuilds the room * under a fresh epoch and everybody in it loses their cursors. */ await expect.poll(() => said).toContain("Only https:, mailto:, relative paths are allowed here."); await expect(content(page).locator("a")).toHaveCount(0); await written(page, room, /^Read the docs\.$/m); });