Fix flaky e2e tests: CI-realistic timeouts + retry-safe kanban field label (#22701)
# 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)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22701?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -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'],
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user