From 3c605eab52ba20989d34803e7b7b5ed1834c5d5c Mon Sep 17 00:00:00 2001 From: Alcahest Date: Sat, 12 Sep 2026 11:29:37 +0200 Subject: [PATCH 1/2] test: a menu title toggles, so opening one closes what was open --- e2e/lib/menu.js | 32 +++++++++++++++++++++++++++----- e2e/specs/language.spec.js | 3 ++- e2e/specs/menu-submenus.spec.js | 20 ++++++++++++++++++++ e2e/wdio.conf.js | 2 +- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/e2e/lib/menu.js b/e2e/lib/menu.js index b134839a..5458122d 100644 --- a/e2e/lib/menu.js +++ b/e2e/lib/menu.js @@ -8,6 +8,7 @@ */ import { browser } from "@wdio/globals"; +import { pressKey } from "./input.js"; import { waitFor } from "./proc.js"; const present = (selector) => @@ -36,6 +37,31 @@ export async function intoList(click, token) { } } +/** + * Open a menu by its title, from whatever state the menubar was left in. + * + * A click on a menu title **toggles** it, so a menu somebody left open makes the next open close it + * instead, and the caller then waits out its whole timeout for an item that was one click away. + * Closing first costs one query when nothing is open. Written as a hazard removed rather than a + * cause proved: `language.spec.js` went red on the runner waiting thirty seconds for the View menu, + * with both of the app's beats regular through it, and this is the one way that can happen from + * outside the app. See BACKLOG.md N174. + */ +export async function openMenu(click, menu) { + if (await present(".menubar__menu")) { + pressKey("Escape"); + await waitFor(async () => ((await present(".menubar__menu")) ? null : 1), { + timeout: 15000, + message: "the menu that was already open to close before another one is asked for", + }); + } + await click(`.menubar__title--${menu}`); + await waitFor(() => present(".menubar__menu"), { + timeout: 15000, + message: `the ${menu} menu to open`, + }); +} + /** * Run a command from the menu that draws it, given the caller's own clicker. * @@ -45,11 +71,7 @@ export async function intoList(click, token) { * for the button that happens to carry it today. */ export async function runFromMenu(click, menu, token) { - await click(`.menubar__title--${menu}`); - await waitFor(() => present(".menubar__menu"), { - timeout: 15000, - message: `the ${menu} menu to open`, - }); + await openMenu(click, menu); await intoList(click, token); await click(`.menubar__item--${token}`); await waitFor(async () => ((await present(".menubar__menu")) ? null : 1), { diff --git a/e2e/specs/language.spec.js b/e2e/specs/language.spec.js index 4232baa0..83277244 100644 --- a/e2e/specs/language.spec.js +++ b/e2e/specs/language.spec.js @@ -12,6 +12,7 @@ import { browser, expect } from "@wdio/globals"; import { clickAt, focusWindow, pressKey } from "../lib/input.js"; import { windowHeight, windowWidth } from "../lib/paths.js"; +import { openMenu } from "../lib/menu.js"; import { waitFor } from "../lib/proc.js"; import { findToplevel } from "../lib/x11.js"; @@ -48,7 +49,7 @@ async function clickElement(toplevel, selector) { } async function openLanguage(toplevel) { - await clickElement(toplevel, ".menubar__title--view"); + await openMenu((css) => clickElement(toplevel, css), "view"); await waitFor(() => present(".menubar__item--view-language"), { timeout: 15000, message: "the View menu to open on its Language item", diff --git a/e2e/specs/menu-submenus.spec.js b/e2e/specs/menu-submenus.spec.js index 368a34a6..54928529 100644 --- a/e2e/specs/menu-submenus.spec.js +++ b/e2e/specs/menu-submenus.spec.js @@ -19,6 +19,7 @@ import { browser, expect } from "@wdio/globals"; import { answerChooser, waitForChooser } from "../lib/chooser.js"; import { clickAt, focusWindow, hoverAt, pressKey } from "../lib/input.js"; import { takeCommands, watchCommands } from "../lib/ipc.js"; +import { openMenu } from "../lib/menu.js"; import { repoRoot, windowHeight, windowWidth } from "../lib/paths.js"; import { waitFor } from "../lib/proc.js"; import { findToplevel } from "../lib/x11.js"; @@ -280,4 +281,23 @@ describe("a menu row that opens a list of its own", () => { await closeMenus(); }); + + it("opens a menu from the menubar however the last check left it", async () => { + // A click on a menu title toggles it, so a menu left open makes the next open close it and the + // caller waits out its whole timeout for an item one click away. `openMenu` closes first, and + // this is the check that says so: the File menu is asked for with the Timing menu already up. + await clickElement(toplevel, ".menubar__title--timing"); + await waitFor(() => present(".menubar__menu"), { + timeout: 15000, + message: "the Timing menu to be left open, which is this check's own precondition", + }); + + await openMenu((css) => clickElement(toplevel, css), "file"); + await waitFor(() => present(".menubar__item--file-open-subtitle"), { + timeout: 15000, + message: "the File menu to open over the one that was already up", + }); + + await closeMenus(); + }); }); diff --git a/e2e/wdio.conf.js b/e2e/wdio.conf.js index 076a6369..6f47e639 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 = 514; +const EXPECTED_TESTS = 515; /** How long mocha lets one test live. Every wait inside a test must be shorter. See N165. */ const TEST_LIMIT_MS = 60000; From c1eefad6828eecda2775054eafaf07ba09040173 Mon Sep 17 00:00:00 2001 From: Alcahest Date: Sat, 12 Sep 2026 11:38:04 +0200 Subject: [PATCH 2/2] test: open a menu from whatever state the menubar was left in --- e2e/specs/menu-submenus.spec.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/e2e/specs/menu-submenus.spec.js b/e2e/specs/menu-submenus.spec.js index 54928529..9928efa6 100644 --- a/e2e/specs/menu-submenus.spec.js +++ b/e2e/specs/menu-submenus.spec.js @@ -282,21 +282,19 @@ describe("a menu row that opens a list of its own", () => { await closeMenus(); }); - it("opens a menu from the menubar however the last check left it", async () => { - // A click on a menu title toggles it, so a menu left open makes the next open close it and the - // caller waits out its whole timeout for an item one click away. `openMenu` closes first, and - // this is the check that says so: the File menu is asked for with the Timing menu already up. + it("opens a menu that is already open, rather than toggling it shut", async () => { + // A click on the title of the menu that is **already** open closes it: clicking another title + // switches, which is why this check names the same one twice. A caller that asked for a menu + // somebody had left open would then wait out its whole timeout for an item one click away. + // `openMenu` closes first and opens after, so the answer does not depend on what came before. await clickElement(toplevel, ".menubar__title--timing"); await waitFor(() => present(".menubar__menu"), { timeout: 15000, message: "the Timing menu to be left open, which is this check's own precondition", }); - await openMenu((css) => clickElement(toplevel, css), "file"); - await waitFor(() => present(".menubar__item--file-open-subtitle"), { - timeout: 15000, - message: "the File menu to open over the one that was already up", - }); + await openMenu((css) => clickElement(toplevel, css), "timing"); + expect(await present(".menubar__menu")).toBe(true); await closeMenus(); });