From 2a263aeecd5b9c139a576db0665a651770a8a0a2 Mon Sep 17 00:00:00 2001 From: Aa99841 Date: Fri, 23 Oct 2026 22:05:05 +0800 Subject: [PATCH 1/3] Switch to the pen when choosing a drawing color Choosing a color while erasing leaves the eraser active, so drawing still erases instead of using the selected color. Make selecting a color return to the pen and keep the toolbar state consistent. --- tests/browser/example-board-ui.test.js | 76 ++++++++++++++++++++++++++ web/example-board.js | 22 ++++++-- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/tests/browser/example-board-ui.test.js b/tests/browser/example-board-ui.test.js index 9eaaaf9b0..26424c111 100644 --- a/tests/browser/example-board-ui.test.js +++ b/tests/browser/example-board-ui.test.js @@ -38,6 +38,82 @@ function drawerMarkup() { return `
Add a case
${exampleMarkup()}
`; } +test("selecting an example pen color switches from erasing to drawing", async (t) => { + if (!browser) return t.skip("playwright chromium unavailable"); + const page = await browser.newPage(); + t.after(() => page.close()); + await page.goto(base); + await page.setContent(exampleMarkup()); + await page.evaluate(async (base) => { + const { mountExampleBoard } = await import(`${base}/example-board.js`); + window.ops = []; + const root = document.querySelector("#example-board"); + root.open = true; + mountExampleBoard(root, { + locked: () => false, + connected: () => false, + record: (ops) => window.ops.push(...ops), + publish: async () => true, + }); + }, base); + + const pen = page.getByRole("button", { name: "Pen", exact: true }); + const eraser = page.getByRole("button", { name: "Eraser", exact: true }); + const red = page.getByRole("button", { name: "Pen Red", exact: true }); + const canvas = page.locator("canvas"); + async function draw() { + const box = await canvas.boundingBox(); + await page.mouse.move(box.x + 20, box.y + 20); + await page.mouse.down(); + await page.mouse.move(box.x + 80, box.y + 40); + await page.mouse.up(); + } + + await eraser.click(); + await red.click(); + assert.equal(await pen.getAttribute("aria-pressed"), "true"); + assert.equal(await eraser.getAttribute("aria-pressed"), "false"); + assert.equal(await red.getAttribute("aria-pressed"), "true"); + assert.equal(await page.evaluate(() => window.ops.length), 0); + await draw(); + assert.deepEqual( + await page.evaluate(() => + window.ops.map(({ color, width }) => ({ color, width })), + ), + [{ color: "#c2261b", width: 3 }], + ); + + await eraser.click(); + assert.equal(await pen.getAttribute("aria-pressed"), "false"); + assert.equal(await eraser.getAttribute("aria-pressed"), "true"); + assert.equal(await red.getAttribute("aria-pressed"), "true"); + await draw(); + assert.deepEqual( + await page.evaluate(() => { + const { color, width } = window.ops.at(-1); + return { color, width }; + }), + { color: "#ffffff", width: 24 }, + ); + await page.getByRole("button", { name: "Undo", exact: true }).click(); + assert.equal(await page.evaluate(() => window.ops.at(-1).op), "undo"); + + await red.click(); + await draw(); + await eraser.click(); + await pen.click(); + await draw(); + assert.deepEqual( + await page.evaluate(() => + window.ops.slice(-2).map(({ color, width }) => ({ color, width })), + ), + [ + { color: "#c2261b", width: 3 }, + { color: "#c2261b", width: 3 }, + ], + ); +}); + test("the example drawer preserves code, journals pen strokes, and sends the cleared state after reconnecting", async (t) => { if (!browser) return t.skip("playwright chromium unavailable"); const page = await browser.newPage({ diff --git a/web/example-board.js b/web/example-board.js index 004b70499..777b31ba1 100644 --- a/web/example-board.js +++ b/web/example-board.js @@ -128,17 +128,26 @@ export function mountExampleBoard( schedule(); } + function paintToolState() { + for (const button of tools.querySelectorAll("button")) + button.setAttribute("aria-pressed", String(button.dataset.tool === tool)); + for (const button of root.querySelectorAll(".board-color")) + button.setAttribute( + "aria-pressed", + String(button.dataset.color === color), + ); + } + for (const name of ["pen", "eraser"]) { const button = document.createElement("button"); button.type = "button"; button.className = "board-tool"; + button.dataset.tool = name; setToolIcon(button, name); - button.setAttribute("aria-pressed", String(name === tool)); button.addEventListener("click", () => { if (drag) return; tool = name; - for (const other of tools.querySelectorAll("button")) - other.setAttribute("aria-pressed", String(other === button)); + paintToolState(); }); tools.append(button); } @@ -146,17 +155,18 @@ export function mountExampleBoard( const button = document.createElement("button"); button.type = "button"; button.className = "board-color"; + button.dataset.color = value; button.style.background = value; button.setAttribute("aria-label", `Pen ${name}`); - button.setAttribute("aria-pressed", String(value === color)); button.addEventListener("click", () => { if (drag) return; color = value; - for (const other of root.querySelectorAll(".board-color")) - other.setAttribute("aria-pressed", String(other === button)); + tool = "pen"; + paintToolState(); }); root.querySelector("[data-colors]").append(button); } + paintToolState(); for (const action of ["undo", "clear"]) { const button = root.querySelector(`[data-action=${action}]`); setToolIcon(button, action); From 38fce474f652a108567c311a7869477a44c1dd9e Mon Sep 17 00:00:00 2001 From: Aa99841 Date: Sat, 24 Oct 2026 11:30:02 +0800 Subject: [PATCH 2/3] Test the full draw-erase-color sequence The existing regression test starts by selecting Eraser without first drawing or erasing a stroke. Add those steps to match the reported interaction and the requested reproduction sequence. Verify that the extended test fails against the upstream board implementation and passes with the fix. Production code is unchanged. --- tests/browser/example-board-ui.test.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/browser/example-board-ui.test.js b/tests/browser/example-board-ui.test.js index 26424c111..056b08e9d 100644 --- a/tests/browser/example-board-ui.test.js +++ b/tests/browser/example-board-ui.test.js @@ -69,19 +69,29 @@ test("selecting an example pen color switches from erasing to drawing", async (t await page.mouse.up(); } + await draw(); + assert.equal(await page.evaluate(() => window.ops.length), 1); await eraser.click(); + await draw(); + assert.deepEqual( + await page.evaluate(() => { + const { color, width } = window.ops.at(-1); + return { color, width }; + }), + { color: "#ffffff", width: 24 }, + ); await red.click(); - assert.equal(await pen.getAttribute("aria-pressed"), "true"); - assert.equal(await eraser.getAttribute("aria-pressed"), "false"); - assert.equal(await red.getAttribute("aria-pressed"), "true"); - assert.equal(await page.evaluate(() => window.ops.length), 0); + assert.equal(await page.evaluate(() => window.ops.length), 2); await draw(); assert.deepEqual( await page.evaluate(() => - window.ops.map(({ color, width }) => ({ color, width })), + window.ops.slice(2).map(({ color, width }) => ({ color, width })), ), [{ color: "#c2261b", width: 3 }], ); + assert.equal(await pen.getAttribute("aria-pressed"), "true"); + assert.equal(await eraser.getAttribute("aria-pressed"), "false"); + assert.equal(await red.getAttribute("aria-pressed"), "true"); await eraser.click(); assert.equal(await pen.getAttribute("aria-pressed"), "false"); From cbd854b1cca0fb9cbc0eda8b48867a793a634d71 Mon Sep 17 00:00:00 2001 From: Aa99841 Date: Mon, 26 Oct 2026 00:26:17 +0800 Subject: [PATCH 3/3] Assert exactly one pen color is selected Check the initial color and red selection exclusively, so stale pressed states on other colors cannot pass the browser test. --- tests/browser/example-board-ui.test.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/browser/example-board-ui.test.js b/tests/browser/example-board-ui.test.js index 056b08e9d..b610fbf89 100644 --- a/tests/browser/example-board-ui.test.js +++ b/tests/browser/example-board-ui.test.js @@ -60,6 +60,14 @@ test("selecting an example pen color switches from erasing to drawing", async (t const pen = page.getByRole("button", { name: "Pen", exact: true }); const eraser = page.getByRole("button", { name: "Eraser", exact: true }); const red = page.getByRole("button", { name: "Pen Red", exact: true }); + const pressedColors = () => + page + .locator(".board-color") + .evaluateAll((buttons) => + buttons + .filter((button) => button.getAttribute("aria-pressed") === "true") + .map((button) => button.getAttribute("aria-label")), + ); const canvas = page.locator("canvas"); async function draw() { const box = await canvas.boundingBox(); @@ -69,6 +77,7 @@ test("selecting an example pen color switches from erasing to drawing", async (t await page.mouse.up(); } + assert.deepEqual(await pressedColors(), ["Pen Black"]); await draw(); assert.equal(await page.evaluate(() => window.ops.length), 1); await eraser.click(); @@ -91,12 +100,12 @@ test("selecting an example pen color switches from erasing to drawing", async (t ); assert.equal(await pen.getAttribute("aria-pressed"), "true"); assert.equal(await eraser.getAttribute("aria-pressed"), "false"); - assert.equal(await red.getAttribute("aria-pressed"), "true"); + assert.deepEqual(await pressedColors(), ["Pen Red"]); await eraser.click(); assert.equal(await pen.getAttribute("aria-pressed"), "false"); assert.equal(await eraser.getAttribute("aria-pressed"), "true"); - assert.equal(await red.getAttribute("aria-pressed"), "true"); + assert.deepEqual(await pressedColors(), ["Pen Red"]); await draw(); assert.deepEqual( await page.evaluate(() => {