|
| 1 | +import { describe, it, expect } from "vitest"; |
| 2 | +import { readFileSync } from "fs"; |
| 3 | +import { resolve } from "path"; |
| 4 | + |
| 5 | +/** |
| 6 | + * Regression tests for issue #206: toggle blocks not functioning as toggles. |
| 7 | + * |
| 8 | + * The root cause was threefold: |
| 9 | + * 1. No visual toggle indicator (chevron) — list-none removed the browser |
| 10 | + * disclosure triangle with no replacement. |
| 11 | + * 2. select-none on <summary> prevented text editing in the title. |
| 12 | + * 3. Native <summary> click toggled <details>, conflicting with Lexical |
| 13 | + * text editing — clicking to place cursor collapsed the section. |
| 14 | + * |
| 15 | + * These static tests verify the structural fixes remain in place. |
| 16 | + */ |
| 17 | + |
| 18 | +function readSource(relativePath: string): string { |
| 19 | + return readFileSync(resolve(__dirname, relativePath), "utf-8"); |
| 20 | +} |
| 21 | + |
| 22 | +describe("collapsible-node toggle affordance", () => { |
| 23 | + const source = readSource("./collapsible-node.tsx"); |
| 24 | + |
| 25 | + it("summary does not use select-none (title must be editable)", () => { |
| 26 | + // select-none prevents cursor placement and text selection in the |
| 27 | + // title, breaking inline editing. |
| 28 | + const summaryClass = source.match(/summary\.className\s*=\s*\n?\s*"([^"]*)"/); |
| 29 | + expect(summaryClass).not.toBeNull(); |
| 30 | + expect(summaryClass![1]).not.toContain("select-none"); |
| 31 | + }); |
| 32 | + |
| 33 | + it("summary includes a toggle chevron button", () => { |
| 34 | + // A dedicated button with class collapsible-toggle must be created |
| 35 | + // as the visual affordance for expand/collapse. |
| 36 | + expect(source).toContain("collapsible-toggle"); |
| 37 | + expect(source).toContain('aria-label", "Toggle section"'); |
| 38 | + }); |
| 39 | + |
| 40 | + it("chevron button is not contentEditable", () => { |
| 41 | + // The chevron must be excluded from Lexical's editable content |
| 42 | + // so it doesn't interfere with text editing. |
| 43 | + expect(source).toContain('contentEditable = "false"'); |
| 44 | + }); |
| 45 | + |
| 46 | + it("summary uses flex layout for chevron + text alignment", () => { |
| 47 | + const summaryClass = source.match(/summary\.className\s*=\s*\n?\s*"([^"]*)"/); |
| 48 | + expect(summaryClass).not.toBeNull(); |
| 49 | + expect(summaryClass![1]).toContain("flex"); |
| 50 | + expect(summaryClass![1]).toContain("items-center"); |
| 51 | + }); |
| 52 | +}); |
| 53 | + |
| 54 | +describe("collapsible-plugin toggle handling", () => { |
| 55 | + const source = readSource("./collapsible-plugin.tsx"); |
| 56 | + |
| 57 | + it("prevents native summary click from toggling details", () => { |
| 58 | + // The plugin must call preventDefault on summary clicks (except |
| 59 | + // on the chevron) to stop the native <details> toggle from |
| 60 | + // conflicting with Lexical text editing. |
| 61 | + expect(source).toContain("e.preventDefault()"); |
| 62 | + expect(source).toContain("handleSummaryClick"); |
| 63 | + }); |
| 64 | + |
| 65 | + it("toggles via chevron button click only", () => { |
| 66 | + // A dedicated chevron click handler must toggle the node state |
| 67 | + // and sync the DOM. |
| 68 | + expect(source).toContain("handleChevronClick"); |
| 69 | + expect(source).toContain("collapsible-toggle"); |
| 70 | + }); |
| 71 | + |
| 72 | + it("syncs DOM open state when toggling via chevron", () => { |
| 73 | + // After updating the Lexical node, the DOM must be synced since |
| 74 | + // we prevented the native toggle behavior. |
| 75 | + expect(source).toContain("dom.open = newOpen"); |
| 76 | + }); |
| 77 | +}); |
| 78 | + |
| 79 | +describe("collapsible chevron CSS rotation", () => { |
| 80 | + const css = readFileSync( |
| 81 | + resolve(__dirname, "../../app/globals.css"), |
| 82 | + "utf-8" |
| 83 | + ); |
| 84 | + |
| 85 | + it("rotates chevron when details is open", () => { |
| 86 | + expect(css).toContain("details[open] > summary > .collapsible-toggle"); |
| 87 | + expect(css).toContain("rotate(90deg)"); |
| 88 | + }); |
| 89 | +}); |
0 commit comments