From eb9cc1862a85ae89b2e4805e9d4a46aa9623699b Mon Sep 17 00:00:00 2001 From: martmull Date: Thu, 9 Jul 2026 10:58:16 +0200 Subject: [PATCH] Fix flaky e2e tests: CI-realistic timeouts + retry-safe kanban field label (#22701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Context Part of a CI flakiness sweep. `ci-e2e-main` fails on ~8% of main pushes (15 failing runs on 2026-07-08 alone), always the same three tests, always on 5-second `expect` timeouts: - `onboarding.spec.ts` — `Create your workspace` heading after sign-up (sign-up mutation → loadCurrentUser → workspace-creation-defaults query, three sequential round trips) - `signup_invite_email.spec.ts` — `Create profile` (invite sign-up + token exchange + full metadata load) - `create-kanban-view.spec.ts` — `No Value` kanban column (view + viewFields + viewGroups persistence, no-value group settles last) The runner hosts the dev-mode NestJS server (`nest start --watch`), the worker and Chromium simultaneously, so 5s is structurally too tight; neighboring steps in the same specs already use 30-90s timeouts. # Fix - `playwright.config.ts`: `expect.timeout` 5s → 15s and test timeout 30s → 60s **on CI only** (the config already branches on `process.env.CI` for retries/reporter). These are web-first auto-retrying assertions, so green runs are not slowed — only genuinely failing assertions wait longer. The 60s test budget also fixes an existing inconsistency: the kanban spec has a 30s per-assertion timeout inside a 30s test budget. - `create-kanban-view.spec.ts`: use a per-run unique label for the Industry select field. The spec is `test.describe.serial`, and Playwright retries re-run the whole group against the same database (no reset between in-run retries). The already-created `Industry` field made the label-uniqueness validation fail permanently, so Save stayed disabled and **every retry of this group failed deterministically** ("element is not enabled" after 30s) — retries were dead weight for this spec. Adversarially reviewed: the alternative (per-assertion timeouts) is the whack-a-mole pattern already attempted once (`Food` has a 30s patch); `waitForResponse` on operation names would be more lines and more brittle. Test-infra only, 2 files, +11/-6. Note for the team (out of scope here): `ci-e2e-main.yaml` builds the server and then discards it — `nx start twenty-server` runs `rimraf dist && NODE_ENV=development nest start --watch`. Running the built server would cut latency and runner load across the whole suite. --- _Generated by [Claude Code](https://claude.ai/code/session_01AtD2wWm3EthV6t3Hs31QyB)_ Review in cubic --- packages/twenty-e2e-testing/playwright.config.ts | 6 ++++-- .../tests/create-kanban-view.spec.ts | 11 +++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts index 8ce700ec11..a57098b377 100644 --- a/packages/twenty-e2e-testing/playwright.config.ts +++ b/packages/twenty-e2e-testing/playwright.config.ts @@ -23,7 +23,7 @@ export default defineConfig({ forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: 1, // 1 worker = 1 test at the time, tests can't be parallelized - timeout: 30 * 1000, // timeout can be changed + timeout: process.env.CI ? 60_000 : 30 * 1000, // timeout can be changed use: { baseURL: process.env.FRONTEND_BASE_URL || 'http://localhost:3001', trace: 'retain-on-failure', // trace takes EVERYTHING from page source, records every single step, should be used only when normal debugging won't work @@ -32,7 +32,9 @@ export default defineConfig({ testIdAttribute: 'data-testid', // taken from Twenty source }, expect: { - timeout: 5000, + // CI runners are slow enough that post-mutation UI transitions routinely + // exceed 5s; locally keep the tight budget. + timeout: process.env.CI ? 15_000 : 5000, }, reporter: [ [process.env.CI ? 'github' : 'list'], diff --git a/packages/twenty-e2e-testing/tests/create-kanban-view.spec.ts b/packages/twenty-e2e-testing/tests/create-kanban-view.spec.ts index 31122f071a..0a8a1530e1 100644 --- a/packages/twenty-e2e-testing/tests/create-kanban-view.spec.ts +++ b/packages/twenty-e2e-testing/tests/create-kanban-view.spec.ts @@ -1,5 +1,8 @@ import { expect, test } from '../lib/fixtures/screenshot'; test.describe.serial('Create Kanban View', () => { +// Unique per run: retries of this serial group re-run field creation against +// the same database, and a duplicate label makes the form unsubmittable. +const industryLabel = `Industry ${Date.now()}`; test('Create Industry Select Field', async ({ page }) => { await page.getByTestId('workspace-dropdown').click(); await page.getByRole('link', { name: 'Settings' }).click(); @@ -9,7 +12,7 @@ test('Create Industry Select Field', async ({ page }) => { await page.getByRole('button', { name: 'New Field' }).click(); await page.getByRole('link', { name: 'Select', exact: true }).click(); await page.getByRole('textbox', { name: 'Employees' }).click(); - await page.getByRole('textbox', { name: 'Employees' }).fill('Industry'); + await page.getByRole('textbox', { name: 'Employees' }).fill(industryLabel); await page.getByRole('textbox').nth(1).click(); await page.getByRole('textbox').nth(1).press('ControlOrMeta+a'); await page.getByRole('textbox').nth(1).fill('Food'); @@ -19,8 +22,8 @@ test('Create Industry Select Field', async ({ page }) => { await page.getByRole('button', { name: 'Option 3' }).getByRole('textbox').fill('Travel'); await page.getByRole('button', { name: 'Save' }).click(); await page.waitForURL('**/objects/opportunities'); - await page.waitForSelector('text=Industry'); - await expect(page.getByText('Industry')).toBeVisible(); + await page.waitForSelector(`text=${industryLabel}`); + await expect(page.getByText(industryLabel)).toBeVisible(); }); test('Create Kanban View from Industry Select Field', async ({ page }) => { @@ -32,7 +35,7 @@ test('Create Kanban View from Industry Select Field', async ({ page }) => { await page.getByRole('button', { name: 'Table', exact: true }).click(); await page.getByText('Kanban').click(); await page.locator('[aria-controls="view-picker-kanban-field-options"]').click(); - await page.getByRole('option', { name: 'Industry' }).click(); + await page.getByRole('option', { name: industryLabel }).click(); await page.getByRole('button', { name: 'Create new view' }).click(); await expect(page.getByText('Food')).toBeVisible({ timeout: 30000 }); await expect(page.getByText('Tech')).toBeVisible();