diff --git a/e2e/specs/grid-selection.spec.js b/e2e/specs/grid-selection.spec.js index 369fd63..0262d03 100644 --- a/e2e/specs/grid-selection.spec.js +++ b/e2e/specs/grid-selection.spec.js @@ -57,6 +57,33 @@ async function cellAt(toplevel, position, cell) { } /** The number cell selects and never opens an editor, which is why every gesture below lands on it. */ +/** How many times the grid has been committed, which `CueList.tsx` counts on the element. */ +function gridRenders() { + return browser.execute(() => Number(document.documentElement.dataset.gridRenders) || 0); +} + +/** Wait until the grid stops drawing on its own, so the count below is about the click. */ +async function settle() { + let last = -1; + for (;;) { + const now = await gridRenders(); + if (now === last) { + return; + } + last = now; + await browser.pause(400); + } +} + +/** The list position the cursor is on, or null. */ +function cursorRow() { + return browser.execute(() => { + const row = document.querySelector(".cuelist__row--active"); + const text = row?.querySelector(".cuelist__pos")?.textContent; + return text === undefined || text === null ? null : Number(text); + }); +} + async function clickRow(toplevel, position, modifier = null) { const at = await cellAt(toplevel, position, ".cuelist__pos"); if (modifier === null) { @@ -226,4 +253,28 @@ describe("the grid's selection gestures", () => { }); expect(await present(".statusbar__dirty")).toBe(false); }); + + it("draws nothing again when the click lands on the row the cursor already holds", async () => { + // Interface-spec 7.2: announcing the same selection again is a redundant render in React, and a + // render that happened for nothing cannot be seen from outside. The grid counts its own commits + // on the element, which is what makes this readable at all. See BACKLOG.md N159. + await clickRow(toplevel, 2); + await waitFor(async () => ((await cursorRow()) === 2 ? 1 : null), { + timeout: 15000, + message: "the cursor to land on the second row", + }); + await settle(); + + const before = await gridRenders(); + await clickRow(toplevel, 2); + await browser.pause(1000); + expect(await gridRenders()).toBe(before); + + // The control, and it is what says the counter counts: another row does draw again. + await clickRow(toplevel, 3); + await waitFor(async () => ((await gridRenders()) > before ? 1 : null), { + timeout: 15000, + message: "a click on another row to draw the grid again", + }); + }); }); diff --git a/e2e/wdio.conf.js b/e2e/wdio.conf.js index 6f47e63..50b5038 100644 --- a/e2e/wdio.conf.js +++ b/e2e/wdio.conf.js @@ -44,7 +44,7 @@ import { * Every spec that exists must run. WebdriverIO does not reliably fail a run that executed nothing, * so the count is asserted here. Bump it when you add a test; see e2e/README.md. */ -const EXPECTED_TESTS = 515; +const EXPECTED_TESTS = 516; /** How long mocha lets one test live. Every wait inside a test must be shorter. See N165. */ const TEST_LIMIT_MS = 60000; diff --git a/src/components/CueList.tsx b/src/components/CueList.tsx index cf87382..49e426a 100644 --- a/src/components/CueList.tsx +++ b/src/components/CueList.tsx @@ -89,6 +89,13 @@ export default function CueList({ }: CueListProps) { const listRef = useRef(null); const editorRef = useRef(null); + // Every commit of the grid, counted where a check can read it. Interface-spec 7.2 asks a click on + // the row the cursor already holds to change nothing, and a render that happened for nothing is + // invisible from outside by definition. No dependency list: this counts commits, not values. + useEffect(() => { + const root = document.documentElement; + root.dataset.gridRenders = String((Number(root.dataset.gridRenders) || 0) + 1); + }); const [scrollTop, setScrollTop] = useState(0); const [viewport, setViewport] = useState(0); const [editing, setEditing] = useState(null); diff --git a/src/hooks/useCueSelection.ts b/src/hooks/useCueSelection.ts index c5c5beb..86c740b 100644 --- a/src/hooks/useCueSelection.ts +++ b/src/hooks/useCueSelection.ts @@ -65,7 +65,11 @@ export function useCueSelection(count: number, openId: number): CueSelection { setSelected(runBetween(anchor.current, index)); } else if (how === "plain") { anchor.current = index; - setSelected(new Set([index])); + // The same one row again is the same selection. A new Set with the same member is a new + // object, and React renders everything that reads it for nothing (interface-spec 7.2). + setSelected((current) => + current.size === 1 && current.has(index) ? current : new Set([index]), + ); } }, []);