From 9a899b67dfbf422d3efecaa8ee611aaef791b97e Mon Sep 17 00:00:00 2001 From: "n8n-cat-bot[bot]" <283985454+n8n-cat-bot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:48:55 +0000 Subject: [PATCH] test: Stabilize canvas zoom assertion after route changes (#36694) Co-authored-by: n8n-cat-bot[bot] Co-authored-by: Claude Opus 4.8 --- .../playwright/composables/CanvasComposer.ts | 36 ++++++------------- .../testing/playwright/pages/CanvasPage.ts | 32 +++++++++++++++++ 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/packages/testing/playwright/composables/CanvasComposer.ts b/packages/testing/playwright/composables/CanvasComposer.ts index 0cfadc70535..5bd3624dd7f 100644 --- a/packages/testing/playwright/composables/CanvasComposer.ts +++ b/packages/testing/playwright/composables/CanvasComposer.ts @@ -79,40 +79,24 @@ export class CanvasComposer { async zoomInAndCheckNodes(): Promise { await this.n8n.canvas.getCanvasNodes().first().waitFor(); - const measureFirstNodeWidth = async () => - await this.n8n.page.evaluate(() => { - const firstNode = document.querySelector('[data-test-id="canvas-node"]'); - if (!firstNode) { - throw new Error('Canvas node not found during measurement'); - } - return firstNode.getBoundingClientRect().width; - }); - - // Wait for the animated fit-to-view to settle before capturing the baseline. - let previousWidth = Number.NaN; - await expect - .poll(async () => { - const width = await measureFirstNodeWidth(); - const settled = width === previousWidth; - previousWidth = width; - return settled; - }) - .toBe(true); - - const initialNodeSize = await measureFirstNodeWidth(); + // After a route change the editor runs an animated fit-to-view. Wait for the + // viewport transform to stop moving before capturing the baseline zoom, so we + // don't measure against a pre-animation value. + await this.n8n.canvas.waitForCanvasZoomSettled(); + const initialZoom = await this.n8n.canvas.getCanvasZoomLevel(); for (let i = 0; i < 4; i++) { await this.n8n.canvas.clickZoomInButton(); } - // Poll the width until the animated zoom transition settles. + // Poll the zoom scale until the animated zoom-in transition settles. await expect - .poll(measureFirstNodeWidth, { + .poll(async () => await this.n8n.canvas.getCanvasZoomLevel(), { message: - "Zoom functionality not working: nodes didn't scale properly. " + - `Initial: ${initialNodeSize.toFixed(1)}px`, + "Zoom functionality not working: canvas didn't scale in. " + + `Initial zoom: ${initialZoom.toFixed(3)}`, }) - .toBeGreaterThan(initialNodeSize * 1.5); + .toBeGreaterThan(initialZoom * 1.5); } /** diff --git a/packages/testing/playwright/pages/CanvasPage.ts b/packages/testing/playwright/pages/CanvasPage.ts index e84f32015b8..2880b4475a4 100644 --- a/packages/testing/playwright/pages/CanvasPage.ts +++ b/packages/testing/playwright/pages/CanvasPage.ts @@ -929,6 +929,38 @@ export class CanvasPage extends BasePage { }); } + /** + * Wait for the canvas viewport transform (zoom/pan) to stop changing. + * After a route change the editor runs an animated fit-to-view. Sparse + * expect.poll sampling can catch two equal values in the pre-animation window + * and report "settled" before the animation starts; this checks every frame + * inside the page so it can't miss the in-flight transition. + */ + async waitForCanvasZoomSettled(stableFrames = 5): Promise { + await this.page.waitForFunction( + (needed) => { + const el = document.querySelector('.vue-flow__transformationpane.vue-flow__container'); + if (!el) return false; + const transform = getComputedStyle(el).transform; + const w = window as unknown as { n8nZoomSettle?: { last: string; count: number } }; + const state = (w.n8nZoomSettle ??= { last: '', count: 0 }); + if (transform === state.last) { + state.count += 1; + } else { + state.last = transform; + state.count = 0; + } + if (state.count >= needed) { + delete w.n8nZoomSettle; + return true; + } + return false; + }, + stableFrames, + { polling: 'raf', timeout: 10_000 }, + ); + } + waitingForTriggerEvent() { return this.getExecuteWorkflowButton().getByText('Waiting for trigger event'); }