From 01d014e53dc86584531578b4c102038436f365e6 Mon Sep 17 00:00:00 2001 From: Daniel Zyto Date: Thu, 20 Aug 2026 12:27:23 +0200 Subject: [PATCH 1/2] fix(runner): the sentinel-column assertion survives the overlay clone (DEV-2203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First live execution of #221's preview-recovery rework: the renamed header renders twice — master table and top overlay clone — so the bare getByText was a strict-mode violation precisely when the rename landed. .first() with the reason in place. Verified against prod. Also corrects e2e-live.yml's header comment, which still said weekly canary after #237 made it nightly. --- .github/workflows/e2e-live.yml | 9 ++++----- runner/e2e/preview-recovery.spec.ts | 5 ++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e-live.yml b/.github/workflows/e2e-live.yml index b3833bd45..f58e4af68 100644 --- a/.github/workflows/e2e-live.yml +++ b/.github/workflows/e2e-live.yml @@ -10,11 +10,10 @@ name: E2E — live render (Sandpack bundler) # the deployed app directly. `ai: true` adds the LLM answer checks; # `pkg_pr_new_ref` boots one container at a pkg.pr.new build (DEV-2198 # validation days). -# - schedule — the weekly prod canary (Mon 05:00 UTC), deployed mode with AI -# on. Deploy-time breakage is caught by the post-deploy smoke below; the -# canary exists for external drift — the hosted bundler, npm, the broker, -# docs-bucket rot — which moves on week-scale. Daily would burn container -# hours and turn every bundler blip into a triage. +# - schedule — the nightly prod canary (03:00 UTC, after the 01:00 starter +# matrix). Deploy-time breakage is caught by the post-deploy smoke below; +# the canary catches external drift — the hosted bundler, npm, the broker, +# docs-bucket rot — and, at Dan's call, runs every live-gated test nightly. # - workflow_call with `smoke: true` — the post-deploy subset both deploy # workflows invoke: `--grep @smoke`, one container, a few minutes. # diff --git a/runner/e2e/preview-recovery.spec.ts b/runner/e2e/preview-recovery.spec.ts index addd1fe6a..1988cc161 100644 --- a/runner/e2e/preview-recovery.spec.ts +++ b/runner/e2e/preview-recovery.spec.ts @@ -196,7 +196,10 @@ test("live: breaking and un-breaking a line leaves the grid alone", async ({ pag const at = doc.indexOf("'Company name'") + 1; view.dispatch({ changes: { from: at, to: at + "Company name".length, insert: "Sentinel column" } }); })()`); - await expect(preview.getByText("Sentinel column")).toBeVisible({ timeout: 60_000 }); + // .first(): Handsontable renders a column header twice — the master table + // and the top overlay clone — so a bare getByText is a strict-mode violation + // the moment the rename actually lands (first live run of this rework, DEV-2203). + await expect(preview.getByText("Sentinel column").first()).toBeVisible({ timeout: 60_000 }); // The grid never left, the status never left ready — and, the discriminating bit, // the preview document was never reset behind our back. From fc9d95b7b8980116a617ef9cb5dc6397e2c1a34e Mon Sep 17 00:00:00 2001 From: Daniel Zyto Date: Thu, 20 Aug 2026 12:33:06 +0200 Subject: [PATCH 2/2] fix(runner): filter the sentinel assertion to the visible header instance .first() alone pinned a hidden internal render of the header text and timed out; Handsontable keeps more than one copy in the DOM and DOM order does not promise the visible one. filter({ visible: true }) does. Verified against prod: 1 passed in 5.3s. --- runner/e2e/preview-recovery.spec.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/runner/e2e/preview-recovery.spec.ts b/runner/e2e/preview-recovery.spec.ts index 1988cc161..3018267db 100644 --- a/runner/e2e/preview-recovery.spec.ts +++ b/runner/e2e/preview-recovery.spec.ts @@ -196,10 +196,14 @@ test("live: breaking and un-breaking a line leaves the grid alone", async ({ pag const at = doc.indexOf("'Company name'") + 1; view.dispatch({ changes: { from: at, to: at + "Company name".length, insert: "Sentinel column" } }); })()`); - // .first(): Handsontable renders a column header twice — the master table - // and the top overlay clone — so a bare getByText is a strict-mode violation - // the moment the rename actually lands (first live run of this rework, DEV-2203). - await expect(preview.getByText("Sentinel column").first()).toBeVisible({ timeout: 60_000 }); + // Handsontable renders a column header more than once — the master table + // plus overlay clones, and at least one copy is a hidden internal render — + // so a bare getByText is a strict-mode violation the moment the rename + // actually lands, and a positional .first() can pin the hidden copy. Filter + // to the visible instance (first live run of this rework, DEV-2203). + await expect( + preview.getByText("Sentinel column").filter({ visible: true }).first(), + ).toBeVisible({ timeout: 60_000 }); // The grid never left, the status never left ready — and, the discriminating bit, // the preview document was never reset behind our back.