eb9cc1862a
# 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. -->
59 lines
3.2 KiB
TypeScript
59 lines
3.2 KiB
TypeScript
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();
|
|
await page.getByRole('link', { name: 'Data model' }).click();
|
|
await page.getByRole('link', { name: 'Opportunities' }).click();
|
|
await expect(page.getByRole('button', { name: 'New Field' })).toBeVisible();
|
|
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(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');
|
|
await page.getByRole('button', { name: 'Add option' }).click();
|
|
await page.getByRole('button', { name: 'Option 2' }).getByRole('textbox').fill('Tech');
|
|
await page.getByRole('button', { name: 'Add option' }).click();
|
|
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=${industryLabel}`);
|
|
await expect(page.getByText(industryLabel)).toBeVisible();
|
|
});
|
|
|
|
test('Create Kanban View from Industry Select Field', async ({ page }) => {
|
|
await page.getByRole('link', { name: 'Opportunities' }).click();
|
|
await page.getByRole('button', { name: /All Opportunities/ }).click();
|
|
await page.getByText('Add view').click();
|
|
await page.getByRole('textbox').press('ControlOrMeta+a');
|
|
await page.getByRole('textbox').fill('By industry');
|
|
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: 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();
|
|
await expect(page.getByText('Travel')).toBeVisible();
|
|
await expect(page.getByText('No Value')).toBeVisible();
|
|
const byIndustryElements = await page.locator('text=By industry').all();
|
|
expect(byIndustryElements.length).toBeGreaterThanOrEqual(1);
|
|
for (const element of byIndustryElements) {
|
|
await expect(element).toBeVisible();
|
|
}
|
|
await page.getByText('Options').click();
|
|
await page.getByText('Group', { exact: true }).click();
|
|
await Promise.all([page.getByTestId('hide-group-').click(),
|
|
page.waitForRequest((req) => {
|
|
return req.url().includes('/metadata') &&
|
|
req.method() === 'POST';
|
|
})]);
|
|
await expect(page.getByText('No Value')).not.toBeVisible();
|
|
});
|
|
})
|