From 6ce62bc545589ac6f1387627acb3b2281b35688d Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 17 Feb 2026 20:23:42 +0100 Subject: [PATCH] Replace API calls in e2e tests with UI interactions Use browser-based user actions for test setup and cleanup instead of direct API/fetch calls, making tests exercise the same code paths as real users. Co-Authored-By: Claude Opus 4.6 --- tests/e2e/milestone.test.ts | 6 ++-- tests/e2e/org.test.ts | 4 +-- tests/e2e/readme.test.ts | 7 +++-- tests/e2e/register.test.ts | 12 +++---- tests/e2e/repo.test.ts | 4 +-- tests/e2e/utils.ts | 63 +++++++++++++++++-------------------- 6 files changed, 45 insertions(+), 51 deletions(-) diff --git a/tests/e2e/milestone.test.ts b/tests/e2e/milestone.test.ts index ffc0b50c30..56baddc14f 100644 --- a/tests/e2e/milestone.test.ts +++ b/tests/e2e/milestone.test.ts @@ -1,16 +1,16 @@ import {env} from 'node:process'; import {test, expect} from '@playwright/test'; -import {login, createRepoApi, deleteRepoApi} from './utils.ts'; +import {login, createRepo, deleteRepo} from './utils.ts'; test('create a milestone', async ({page}) => { const repoName = `e2e-milestone-${Date.now()}`; await login(page); - await createRepoApi(page.request, {name: repoName}); + await createRepo(page, 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'); // cleanup - await deleteRepoApi(page.request, env.E2E_USER!, repoName); + await deleteRepo(page, env.E2E_USER!, repoName); }); diff --git a/tests/e2e/org.test.ts b/tests/e2e/org.test.ts index d428832fb5..b790d929b6 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, deleteOrg} from './utils.ts'; test('create an organization', async ({page}) => { const orgName = `e2e-org-${Date.now()}`; @@ -10,5 +10,5 @@ test('create an organization', async ({page}) => { await expect(page).toHaveURL(new RegExp(`/org/${orgName}`)); // cleanup - await deleteOrgApi(page.request, orgName); + await deleteOrg(page, orgName); }); diff --git a/tests/e2e/readme.test.ts b/tests/e2e/readme.test.ts index 1aba762ca2..80d60d6cc6 100644 --- a/tests/e2e/readme.test.ts +++ b/tests/e2e/readme.test.ts @@ -1,13 +1,14 @@ import {env} from 'node:process'; import {test, expect} from '@playwright/test'; -import {createRepoApi, deleteRepoApi} from './utils.ts'; +import {login, createRepo, deleteRepo} from './utils.ts'; test('README renders on repository page', async ({page}) => { const repoName = `e2e-readme-${Date.now()}`; - await createRepoApi(page.request, {name: repoName}); + await login(page); + await createRepo(page, repoName); await page.goto(`/${env.E2E_USER}/${repoName}`); await expect(page.locator('#readme')).toContainText(repoName); // cleanup - await deleteRepoApi(page.request, env.E2E_USER!, repoName); + await deleteRepo(page, env.E2E_USER!, repoName); }); diff --git a/tests/e2e/register.test.ts b/tests/e2e/register.test.ts index 70059355e4..d7975ce081 100644 --- a/tests/e2e/register.test.ts +++ b/tests/e2e/register.test.ts @@ -1,6 +1,5 @@ -import {env} from 'node:process'; import {test, expect} from '@playwright/test'; -import {logout} from './utils.ts'; +import {login, logout, deleteUser} from './utils.ts'; test.beforeEach(async ({page}) => { await page.goto('/user/sign_up'); @@ -53,11 +52,10 @@ test('register then login', async ({page}) => { await page.getByRole('button', {name: 'Sign In'}).click(); await expect(page.getByRole('link', {name: 'Sign In'})).toBeHidden(); - // Clean up: delete the user via API using the main e2e admin account - const response = await page.request.delete(`/api/v1/admin/users/${username}?purge=true`, { - headers: {Authorization: `Basic ${btoa(`${env.E2E_USER}:${env.E2E_PASSWORD}`)}`}, - }); - expect(response.ok()).toBeTruthy(); + // Clean up: login as admin and delete the user via site administration + await logout(page); + await login(page); + await deleteUser(page, username); }); test('register with existing username shows error', async ({page}) => { diff --git a/tests/e2e/repo.test.ts b/tests/e2e/repo.test.ts index 1f9904a893..2183f47dd4 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, deleteRepoApi} from './utils.ts'; +import {login, deleteRepo} from './utils.ts'; test('create a repository', async ({page}) => { const repoName = `e2e-repo-${Date.now()}`; @@ -11,5 +11,5 @@ test('create a repository', async ({page}) => { await expect(page).toHaveURL(new RegExp(`/${env.E2E_USER}/${repoName}$`)); // cleanup - await deleteRepoApi(page.request, env.E2E_USER!, repoName); + await deleteRepo(page, env.E2E_USER!, repoName); }); diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index e4021fa6f8..06806f5043 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -1,46 +1,41 @@ import {env} from 'node:process'; import {expect} from '@playwright/test'; -import type {APIRequestContext, Locator, Page} from '@playwright/test'; +import type {Locator, Page} from '@playwright/test'; -export function apiBaseUrl() { - return env.E2E_URL?.replace(/\/$/g, ''); +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 function apiHeaders() { - return {Authorization: `Basic ${globalThis.btoa(`${env.E2E_USER}:${env.E2E_PASSWORD}`)}`}; +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('**/'); } -async function apiRetry(fn: () => Promise<{ok: () => boolean; status: () => number; text: () => Promise}>, label: string) { - const maxAttempts = 5; - for (let attempt = 0; attempt < maxAttempts; attempt++) { - const response = await fn(); - if (response.ok()) return; - if ([500, 502, 503].includes(response.status()) && attempt < maxAttempts - 1) { - const jitter = Math.random() * 500; - await new Promise((resolve) => globalThis.setTimeout(resolve, 1000 * (attempt + 1) + jitter)); - continue; - } - throw new Error(`${label} failed: ${response.status()} ${await response.text()}`); - } +export async function deleteOrg(page: Page, name: string) { + await page.goto(`/org/${name}/settings`); + await page.locator('button[data-modal="#delete-org-modal"]').click(); + const modal = page.locator('#delete-org-modal'); + await modal.locator('input[name="org_name"]').fill(name); + await modal.getByRole('button', {name: 'Delete This Organization'}).click(); + await page.waitForURL('**/'); } -export async function createRepoApi(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}, - }), 'createRepoApi'); -} - -export async function deleteRepoApi(requestContext: APIRequestContext, owner: string, name: string) { - await apiRetry(() => requestContext.delete(`${apiBaseUrl()}/api/v1/repos/${owner}/${name}`, { - headers: apiHeaders(), - }), 'deleteRepoApi'); -} - -export async function deleteOrgApi(requestContext: APIRequestContext, name: string) { - await apiRetry(() => requestContext.delete(`${apiBaseUrl()}/api/v1/orgs/${name}`, { - headers: apiHeaders(), - }), 'deleteOrgApi'); +export async function deleteUser(page: Page, username: string) { + await page.goto(`/-/admin/users?q=${username}`); + const userRow = page.locator('tr', {has: page.locator(`a[href="/${username}"]`)}); + await userRow.locator('a[data-tooltip-content="Edit"]').click(); + await page.locator('button[data-modal="#delete-user-modal"]').click(); + const modal = page.locator('#delete-user-modal'); + await modal.locator('input[name="purge"]').check(); + await modal.locator('.ok.button').click(); + await page.waitForURL('**/-/admin/users'); } export async function clickDropdownItem(page: Page, trigger: Locator, itemText: string) {