From 766ba0184aef4d7a6477a11b0561222ac69f999d Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 18 Feb 2026 03:21:28 +0100 Subject: [PATCH] Revert e2e repo create/delete to API calls, double timeouts Revert createRepo/deleteRepo to API-based functions for test reliability. The UI-based versions were flaky due to navigation timing. Also double all playwright timeouts (local and CI), rename API functions to apiX convention, and disable playwright/expect-expect lint rule. Co-Authored-By: Claude Opus 4.6 --- eslint.config.ts | 1 + playwright.config.ts | 8 ++++---- tests/e2e/login.test.ts | 2 +- tests/e2e/milestone.test.ts | 6 +++--- tests/e2e/org.test.ts | 4 ++-- tests/e2e/readme.test.ts | 7 +++---- tests/e2e/repo.test.ts | 8 ++++---- tests/e2e/utils.ts | 25 +++++++++++-------------- 8 files changed, 29 insertions(+), 32 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index 84071312fc..49fcae22d6 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -913,6 +913,7 @@ export default defineConfig([ files: ['tests/e2e/**/*.test.ts'], rules: { ...playwright.configs['flat/recommended'].rules, + 'playwright/expect-expect': [0], }, }, { diff --git a/playwright.config.ts b/playwright.config.ts index 613a3a7ad2..4df9213440 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,15 +7,15 @@ export default defineConfig({ testMatch: /.*\.test\.ts/, forbidOnly: Boolean(env.CI), reporter: 'list', - timeout: env.CI ? 6000 : 2000, + timeout: env.CI ? 12000 : 6000, expect: { - timeout: env.CI ? 3000 : 1000, + timeout: env.CI ? 6000 : 3000, }, use: { baseURL: env.E2E_URL?.replace?.(/\/$/g, ''), locale: 'en-US', - actionTimeout: env.CI ? 3000 : 1000, - navigationTimeout: env.CI ? 6000 : 2000, + actionTimeout: env.CI ? 6000 : 3000, + navigationTimeout: env.CI ? 12000 : 6000, }, projects: [ { diff --git a/tests/e2e/login.test.ts b/tests/e2e/login.test.ts index 8a8efabc21..ecf80d2474 100644 --- a/tests/e2e/login.test.ts +++ b/tests/e2e/login.test.ts @@ -6,7 +6,7 @@ test('homepage', async ({page}) => { await expect(page.getByRole('img', {name: 'Logo'})).toHaveAttribute('src', '/assets/img/logo.svg'); }); -test('login and logout', async ({page}) => { // eslint-disable-line playwright/expect-expect +test('login and logout', async ({page}) => { await login(page); await logout(page); }); diff --git a/tests/e2e/milestone.test.ts b/tests/e2e/milestone.test.ts index 3cd9acfc61..68d5e9b3d0 100644 --- a/tests/e2e/milestone.test.ts +++ b/tests/e2e/milestone.test.ts @@ -1,14 +1,14 @@ import {env} from 'node:process'; import {test, expect} from '@playwright/test'; -import {login, createRepo, deleteRepo} from './utils.ts'; +import {login, apiCreateRepo, apiDeleteRepo} from './utils.ts'; test('create a milestone', async ({page}) => { const repoName = `e2e-milestone-${Date.now()}`; await login(page); - await createRepo(page, repoName); + await apiCreateRepo(page.request, {name: repoName}); await page.goto(`/${env.E2E_USER}/${repoName}/milestones/new`); await page.getByPlaceholder('Title').fill('Test Milestone'); await page.getByRole('button', {name: 'Create Milestone'}).click(); await expect(page.locator('.milestone-list')).toContainText('Test Milestone'); - await deleteRepo(page, env.E2E_USER, repoName); + await apiDeleteRepo(page.request, env.E2E_USER, repoName); }); diff --git a/tests/e2e/org.test.ts b/tests/e2e/org.test.ts index 8160fdda10..b4d4fc2e7d 100644 --- a/tests/e2e/org.test.ts +++ b/tests/e2e/org.test.ts @@ -1,5 +1,5 @@ import {test, expect} from '@playwright/test'; -import {login, deleteOrgApi} from './utils.ts'; +import {login, apiDeleteOrg} from './utils.ts'; test('create an organization', async ({page}) => { const orgName = `e2e-org-${Date.now()}`; @@ -9,5 +9,5 @@ test('create an organization', async ({page}) => { await page.getByRole('button', {name: 'Create Organization'}).click(); await expect(page).toHaveURL(new RegExp(`/org/${orgName}`)); // delete via API because of issues related to form-fetch-action - await deleteOrgApi(page.request, orgName); + await apiDeleteOrg(page.request, orgName); }); diff --git a/tests/e2e/readme.test.ts b/tests/e2e/readme.test.ts index d5e010af58..999a280f1e 100644 --- a/tests/e2e/readme.test.ts +++ b/tests/e2e/readme.test.ts @@ -1,12 +1,11 @@ import {env} from 'node:process'; import {test, expect} from '@playwright/test'; -import {login, createRepo, deleteRepo} from './utils.ts'; +import {apiCreateRepo, apiDeleteRepo} from './utils.ts'; test('README renders on repository page', async ({page}) => { const repoName = `e2e-readme-${Date.now()}`; - await login(page); - await createRepo(page, repoName); + await apiCreateRepo(page.request, {name: repoName}); await page.goto(`/${env.E2E_USER}/${repoName}`); await expect(page.locator('#readme')).toContainText(repoName); - await deleteRepo(page, env.E2E_USER, repoName); + await apiDeleteRepo(page.request, env.E2E_USER, repoName); }); diff --git a/tests/e2e/repo.test.ts b/tests/e2e/repo.test.ts index db800da88e..1df024511c 100644 --- a/tests/e2e/repo.test.ts +++ b/tests/e2e/repo.test.ts @@ -1,6 +1,6 @@ import {env} from 'node:process'; -import {test, expect} from '@playwright/test'; -import {login, deleteRepo} from './utils.ts'; +import {test} from '@playwright/test'; +import {login, apiDeleteRepo} from './utils.ts'; test('create a repository', async ({page}) => { const repoName = `e2e-repo-${Date.now()}`; @@ -8,6 +8,6 @@ test('create a repository', async ({page}) => { await page.goto('/repo/create'); await page.locator('input[name="repo_name"]').fill(repoName); await page.getByRole('button', {name: 'Create Repository'}).click(); - await expect(page).toHaveURL(new RegExp(`/${env.E2E_USER}/${repoName}$`)); - await deleteRepo(page, env.E2E_USER, repoName); + await page.waitForURL(new RegExp(`/${env.E2E_USER}/${repoName}$`)); + await apiDeleteRepo(page.request, env.E2E_USER, repoName); }); diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index 5603c7c939..4ba8cc9d73 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -24,26 +24,23 @@ async function apiRetry(fn: () => Promise<{ok: () => boolean; status: () => numb } } -export async function createRepo(page: Page, name: string) { - await page.goto('/repo/create'); - await page.locator('input[name="repo_name"]').fill(name); - await page.locator('input[name="auto_init"]').check(); - await page.getByRole('button', {name: 'Create Repository'}).click(); +export async function apiCreateRepo(requestContext: APIRequestContext, {name, autoInit = true}: {name: string; autoInit?: boolean}) { + await apiRetry(() => requestContext.post(`${apiBaseUrl()}/api/v1/user/repos`, { + headers: apiHeaders(), + data: {name, auto_init: autoInit}, + }), 'apiCreateRepo'); } -export async function deleteRepo(page: Page, owner: string, name: string) { - await page.goto(`/${owner}/${name}/settings`); - await page.locator('button[data-modal="#delete-repo-modal"]').click(); - const modal = page.locator('#delete-repo-modal'); - await modal.locator('input[name="repo_name"]').fill(name); - await modal.getByRole('button', {name: 'Delete Repository'}).click(); - await page.waitForURL('**/'); +export async function apiDeleteRepo(requestContext: APIRequestContext, owner: string, name: string) { + await apiRetry(() => requestContext.delete(`${apiBaseUrl()}/api/v1/repos/${owner}/${name}`, { + headers: apiHeaders(), + }), 'apiDeleteRepo'); } -export async function deleteOrgApi(requestContext: APIRequestContext, name: string) { +export async function apiDeleteOrg(requestContext: APIRequestContext, name: string) { await apiRetry(() => requestContext.delete(`${apiBaseUrl()}/api/v1/orgs/${name}`, { headers: apiHeaders(), - }), 'deleteOrgApi'); + }), 'apiDeleteOrg'); } export async function clickDropdownItem(page: Page, trigger: Locator, itemText: string) {