From d565a1cb12405326300b0537934500e9704b75d3 Mon Sep 17 00:00:00 2001 From: Felix Stubner Date: Mon, 7 Sep 2026 23:53:11 +0100 Subject: [PATCH] test(gui): assert destructive menu items are marked at rest, not on hover The e2e suite failed on a run of the current app: "Exit icon should be neutral before hover", actual rgb(244, 117, 130). The app is right and the assertion was stale. #296 moved the red to rest deliberately, on the reasoning that a warning arriving once you have already aimed at the item is too late to redirect you. That PR changed four source files and no e2e file, so the assertion kept encoding the behaviour it replaced. Renamed from assertExitMenuItemNeutralUntilHover, since the old name asserted the old rule. The expectation now reads `--red` through a probe element rather than a hardcoded hex, so it holds in both themes (#f47582 dark, #c6293d light) and getComputedStyle normalises it to the same rgb() form the icon reports. The probe goes inside `.container`: tokens.css declares the palette there and not on :root, and a first attempt appended it to document.body, where var(--red) resolved to nothing and inherited white. `rawRed` is asserted non-empty so that failure mode reports itself instead of surfacing as a colour mismatch. Hover is still checked, but now asserts the marking STAYS rather than appears. menu.css covers hover as "a subset" of at-rest, so a rule that reverted there would be a regression the at-rest check alone would miss. Verified by running the suite against the built app: exit 0, no assertion errors, and all 12 screenshots rewritten through to tauri-render-narrow.png, which is the last one the run produces. eslint clean. --- .../tauri-render/scenarios/helpers/menu.mjs | 42 +++++++++++++++++-- .../e2e/tauri-render/scenarios/shell.mjs | 4 +- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/apps/netscli-gui/e2e/tauri-render/scenarios/helpers/menu.mjs b/apps/netscli-gui/e2e/tauri-render/scenarios/helpers/menu.mjs index e6197b47..d01e59a5 100644 --- a/apps/netscli-gui/e2e/tauri-render/scenarios/helpers/menu.mjs +++ b/apps/netscli-gui/e2e/tauri-render/scenarios/helpers/menu.mjs @@ -75,18 +75,52 @@ async function assertEmptyWorkspaceState(driver) { assert.doesNotMatch(state.status, /v\d+\./i, 'Version should live in About, not the footer'); } -async function assertExitMenuItemNeutralUntilHover(driver) { +/* A destructive item is marked before you aim at it, not once you have. + * + * This asserted the opposite until now -- "Exit icon should be neutral before + * hover" -- which was correct until #296 moved the red to rest, on the + * reasoning that a warning arriving after you have already aimed at the item + * is too late to redirect you. That PR touched four source files and no e2e + * file, so the assertion kept encoding the behaviour it replaced and failed + * on a run of the current app. + * + * The expected colour is read from `--red` rather than written as a hex, so + * this holds in both themes: the token is #f47582 dark and #c6293d light. A + * probe element resolves it through getComputedStyle, which normalises it to + * the same `rgb(...)` form the icon's own computed colour is reported in. + * + * The probe goes inside `.container`, not on `document.body`. tokens.css + * declares the palette on `.container` (and `.container.theme-light`), not on + * `:root`, so a probe on the body resolves `var(--red)` to nothing and + * inherits white. `rawRed` is asserted non-empty below to catch that directly + * rather than through a confusing colour mismatch, which is how it was found. + */ +async function assertDestructiveMenuItemMarkedAtRest(driver) { await openMenu(driver, 'File'); const colors = await driver.executeScript(` const items = Array.from(document.querySelectorAll('.menu-popover-item')); const normal = items.find((button) => button.textContent.trim() === 'New Port Scan')?.querySelector('svg'); const exit = items.find((button) => button.textContent.trim() === 'Exit')?.querySelector('svg'); + const scope = document.querySelector('.container'); + const probe = document.createElement('span'); + probe.style.cssText = 'color: var(--red); position: absolute; visibility: hidden;'; + scope.appendChild(probe); + const danger = getComputedStyle(probe).color; + probe.remove(); return { normalIcon: normal ? getComputedStyle(normal).color : '', exitIcon: exit ? getComputedStyle(exit).color : '', + danger, + rawRed: getComputedStyle(scope).getPropertyValue('--red').trim(), }; `); - assert.equal(colors.exitIcon, colors.normalIcon, 'Exit icon should be neutral before hover'); + assert.notEqual(colors.rawRed, '', '--red should resolve on .container; if empty, the probe is in the wrong scope'); + assert.equal(colors.exitIcon, colors.danger, 'Exit icon should be danger-coloured at rest'); + assert.notEqual(colors.normalIcon, colors.danger, 'A non-destructive item should not be danger-coloured'); + + // Hover must not take the marking away. menu.css covers hover as "a subset" + // of at-rest, so a rule that reverted here would be a regression the + // at-rest check alone would not see. const exitButton = await driver.findElement(By.xpath("//button[contains(@class, 'menu-popover-item') and normalize-space()='Exit']")); await driver.actions({ async: true }).move({ origin: exitButton }).perform(); const hoverColor = await driver.executeScript(` @@ -95,7 +129,7 @@ async function assertExitMenuItemNeutralUntilHover(driver) { ?.querySelector('svg'); return exit ? getComputedStyle(exit).color : ''; `); - assert.notEqual(hoverColor, colors.exitIcon, 'Exit icon should become danger-colored on hover'); + assert.equal(hoverColor, colors.danger, 'Exit icon should stay danger-coloured on hover'); } async function assertMenuItemDisabled(driver, menuLabel, itemLabel, expected) { @@ -225,8 +259,8 @@ async function assertToolbarButtonDisabled(driver, title, expected) { } export { + assertDestructiveMenuItemMarkedAtRest, assertEmptyWorkspaceState, - assertExitMenuItemNeutralUntilHover, assertInterfaceReadinessReflectsSelection, assertMenuIncludes, assertMenuItemDisabled, diff --git a/apps/netscli-gui/e2e/tauri-render/scenarios/shell.mjs b/apps/netscli-gui/e2e/tauri-render/scenarios/shell.mjs index d8739e2a..0e126665 100644 --- a/apps/netscli-gui/e2e/tauri-render/scenarios/shell.mjs +++ b/apps/netscli-gui/e2e/tauri-render/scenarios/shell.mjs @@ -4,7 +4,7 @@ import { assertCommand, assertNoErrorStrip, assertTheme, clickButtonText, replac import { assertExportArtifactsCreated, countExportArtifacts } from './helpers/export.mjs'; import { assertAboutDialogPolish, assertEmptyStateCentered, assertTrafficArrowsAreLedStyle, forceTabOverflow } from './helpers/polish.mjs'; import { assertCommandStatusAlignment, assertInteractiveCursorTreatment, assertSuppressesNativeContextMenu, assertThemedTooltips, assertToastHasTimeoutBar } from './helpers/interaction.mjs'; -import { assertEmptyWorkspaceState, assertExitMenuItemNeutralUntilHover, assertInterfaceReadinessReflectsSelection, assertMenuIncludes, assertMenuItemDisabled, assertMenuItems, assertMenuKeyboardNavigation, assertToolbarButtonDisabled, clickMenuItem, countTabs, ensureTrafficIndicatorsVisible, getActiveTabText, waitForTabCount } from './helpers/menu.mjs'; +import { assertDestructiveMenuItemMarkedAtRest, assertEmptyWorkspaceState, assertInterfaceReadinessReflectsSelection, assertMenuIncludes, assertMenuItemDisabled, assertMenuItems, assertMenuKeyboardNavigation, assertToolbarButtonDisabled, clickMenuItem, countTabs, ensureTrafficIndicatorsVisible, getActiveTabText, waitForTabCount } from './helpers/menu.mjs'; import { assertSettingsDialog, closeSettingsDialog, openSettingsDialog } from './helpers/settingsDialog.mjs'; import { assertActiveTabVisible, assertDetailPaneCanFillWorkspace, assertEmptyToolLauncherVisible, assertOverflowTabClickSelection, assertTabAddControlPlacement, assertTabOverflowTreatment, assertTabToolPopoverTopLayer, assertTabToolPopoverVisible } from './helpers/tabs.mjs'; @@ -111,7 +111,7 @@ export async function exerciseMenusAndToolbar(driver) { await assertMenuItemDisabled(driver, 'File', 'Export JSON', false); await assertMenuItemDisabled(driver, 'File', 'Export CSV', false); - await assertExitMenuItemNeutralUntilHover(driver); + await assertDestructiveMenuItemMarkedAtRest(driver); await assertMenuItemDisabled(driver, 'History', 'Clear History', false); await assertMenuItemDisabled(driver, 'Scan', 'Run Active Tab', false); await assertMenuItemDisabled(driver, 'Scan', 'Cancel Active Tab', true);