From 4b217179d920ca6979af9b340776441e33cdfef5 Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 1 May 2026 14:07:51 +0100 Subject: [PATCH] fix(test): null padDeletionToken before pad init to stop modal focus theft MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #7546 added a one-time pad-deletion-token modal that opens via the clientVars handshake on creator sessions and synchronously focuses its input through setTimeout(0). `goToNewPad`'s previous mitigation hid the modal element after `waitForEditorReady`, but the editor iframe attaches before clientVars arrives, so the hide runs against a still- hidden modal, short-circuits, and the modal opens later mid-test — stealing focus and dropping the next Enter / Tab. Visible on develop in `enter.spec.ts:33` and `indentation.spec.ts:9` across all four Playwright jobs (run 25214868650). Intercept `clientVars` assignment via `page.addInitScript` and null out `padDeletionToken` before `pad.ts`'s `showDeletionTokenModalIfPresent` can read it, so the modal-show short-circuits at the source. The deletion-token spec navigates inline with `page.goto` and does not call this helper, so its modal still appears. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/tests/frontend-new/helper/padHelper.ts | 40 +++++++++++++--------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/tests/frontend-new/helper/padHelper.ts b/src/tests/frontend-new/helper/padHelper.ts index ecb4410514f..9e1f557c403 100644 --- a/src/tests/frontend-new/helper/padHelper.ts +++ b/src/tests/frontend-new/helper/padHelper.ts @@ -133,25 +133,33 @@ const waitForEditorReady = async (page: Page) => { }; export const goToNewPad = async (page: Page) => { - // create a new pad before each test run + // Suppress the one-time pad-deletion-token modal in tests. The modal + // pops up on creator sessions via the clientVars handshake and steals + // focus through a setTimeout, which races with goToNewPad's + // waitForEditorReady — the editor iframe attaches before clientVars + // arrives, so any post-load DOM hide runs too early and the modal + // still opens mid-test, eating Enter / Tab presses (#7546 regression + // observed in enter.spec and indentation.spec). Intercept clientVars + // assignment instead and null out padDeletionToken before pad.ts can + // read it; that way the modal-show short-circuits at the source. + // Tests that need to interact with the modal navigate inline and do + // not call this helper. + await page.addInitScript(() => { + let stored: unknown; + Object.defineProperty(window, 'clientVars', { + configurable: true, + get() { return stored; }, + set(v) { + if (v != null && typeof v === 'object') { + (v as {padDeletionToken?: string | null}).padDeletionToken = null; + } + stored = v; + }, + }); + }); const padId = "FRONTEND_TESTS"+randomUUID(); await page.goto('http://localhost:9001/p/'+padId); await waitForEditorReady(page); - // Creator sessions see the one-time pad-deletion-token modal on first visit. - // Hide it directly instead of clicking the ack button — clicking the button - // transfers focus out of the pad iframe and breaks subsequent keyboard tests. - // Tests that need to interact with the modal should navigate to a new pad - // inline instead of using this helper. - await page.evaluate(() => { - const modal = document.getElementById('deletiontoken-modal'); - if (modal == null || modal.hidden) return; - modal.hidden = true; - modal.classList.remove('popup-show'); - const input = document.getElementById('deletiontoken-value') as HTMLInputElement | null; - if (input) input.value = ''; - const w = window as unknown as {clientVars?: {padDeletionToken?: string | null}}; - if (w.clientVars != null) w.clientVars.padDeletionToken = null; - }); return padId; }