0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-16 21:17:26 +02:00

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 <noreply@anthropic.com>
This commit is contained in:
silverwind 2026-02-17 20:23:42 +01:00
parent 1e010377f8
commit 6ce62bc545
No known key found for this signature in database
GPG Key ID: 2E62B41C93869443
6 changed files with 45 additions and 51 deletions

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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}) => {

View File

@ -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);
});

View File

@ -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<string>}>, 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) {