test(e2e): add comment, release, star, PR and fork tests (#37800)

Adds Playwright e2e coverage for five high-value workflows, each driven
through semantic locators with API-based setup:

- comment on and close an issue
- publish a release
- star and watch a repository
- create a pull request from the compare page
- fork a repository

Also passes `autoInit: false` in existing tests that only exercise
DB-backed units (issues, reactions, milestones, projects, events),
skipping an unused initial commit to speed up their setup and reduce
parallel git contention.

---
This PR was written with the help of Claude Opus 4.7

---------

Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Nicolas <bircni@icloud.com>
This commit is contained in:
silverwind
2026-05-22 20:52:04 +02:00
committed by GitHub
parent 4c37f4dacb
commit 7c12446c1f
10 changed files with 118 additions and 14 deletions

View File

@@ -12,7 +12,7 @@ test.describe('events', () => {
// Create repo and login in parallel — repo is needed for the issue, login for the event stream
await Promise.all([
apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(owner)}),
apiCreateRepo(request, {name: repoName, autoInit: false, headers: apiUserHeaders(owner)}),
loginUser(page, owner),
]);
await page.goto('/');
@@ -36,7 +36,7 @@ test.describe('events', () => {
await Promise.all([
loginUser(page, name),
(async () => {
await apiCreateRepo(request, {name, headers});
await apiCreateRepo(request, {name, autoInit: false, headers});
await apiCreateIssue(request, {owner: name, repo: name, title: 'events stopwatch test', headers});
await apiStartStopwatch(request, name, name, 1, {headers});
})(),

18
tests/e2e/fork.test.ts Normal file
View File

@@ -0,0 +1,18 @@
import {env} from 'node:process';
import {test, expect} from '@playwright/test';
import {login, apiCreateRepo, apiCreateUser, apiUserHeaders, randomString} from './utils.ts';
test('fork a repository', async ({page, request}) => {
const upstream = `fork-owner-${randomString(8)}`;
const repoName = `e2e-fork-${randomString(8)}`;
await apiCreateUser(request, upstream);
await Promise.all([
apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(upstream)}),
login(page),
]);
await page.goto(`/${upstream}/${repoName}/fork`);
await page.getByRole('button', {name: 'Fork Repository'}).click();
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`));
await expect(page.getByRole('link', {name: `${upstream}/${repoName}`})).toBeVisible();
});

View File

@@ -0,0 +1,24 @@
import {env} from 'node:process';
import {test, expect} from '@playwright/test';
import {login, apiCreateRepo, apiCreateIssue, randomString} from './utils.ts';
test('comment on and close an issue', async ({page, request}) => {
const repoName = `e2e-issue-comment-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await apiCreateRepo(request, {name: repoName, autoInit: false});
await Promise.all([
apiCreateIssue(request, {owner, repo: repoName, title: 'Comment test'}),
login(page),
]);
await page.goto(`/${owner}/${repoName}/issues/1`);
const body = `e2e-comment-${randomString(8)}`;
await page.getByPlaceholder('Leave a comment').fill(body);
// exact match: the status button reads "Close with Comment" while the box has content, which substring-matches "Comment"
await page.getByRole('button', {name: 'Comment', exact: true}).click();
await expect(page.locator('.comment-body').filter({hasText: body})).toBeVisible();
// posting reloaded the page with an empty box, so the status button now reads "Close Issue"
await page.getByRole('button', {name: 'Close Issue'}).click();
await expect(page.getByRole('button', {name: 'Reopen Issue'})).toBeVisible();
});

View File

@@ -5,7 +5,7 @@ import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, createProject, crea
test('assign issue to project and change column', async ({page}) => {
const repoName = `e2e-issue-project-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName, autoInit: false})]);
await page.goto(`/${user}/${repoName}/projects/new`);
await page.locator('input[name="title"]').fill('Kanban Board');
await page.getByRole('button', {name: 'Create Project'}).click();
@@ -33,7 +33,7 @@ test('create a project', async ({page}) => {
const projectTitle = 'Test Project';
await login(page);
await apiCreateRepo(page.request, {name: repoName});
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Navigate to new project page
@@ -62,7 +62,7 @@ test('assign issue to multiple projects via sidebar', async ({page}) => {
const issueTitle = 'Test issue for multiple projects';
await login(page);
await apiCreateRepo(page.request, {name: repoName});
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects via UI
@@ -112,7 +112,7 @@ test('create issue with multiple projects pre-selected', async ({page}) => {
const issueTitle = 'Issue with multiple projects';
await login(page);
await apiCreateRepo(page.request, {name: repoName});
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects via UI
@@ -163,7 +163,7 @@ test('filter issues by multiple projects in issue list', async ({page}) => {
const project2Title = 'Filter Project B';
await login(page);
await apiCreateRepo(page.request, {name: repoName});
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects via UI
@@ -229,7 +229,7 @@ test('remove issue from one project keeping others', async ({page}) => {
const issueTitle = 'Issue to modify projects';
await login(page);
await apiCreateRepo(page.request, {name: repoName});
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects via UI
@@ -288,7 +288,7 @@ test('filter issues with no project using project=-1', async ({page}) => {
const projectTitle = 'Some Project';
await login(page);
await apiCreateRepo(page.request, {name: repoName});
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create a project via UI
@@ -349,7 +349,7 @@ test('close project and view in closed projects list', async ({page}) => {
const closedProjectTitle = 'Project To Close';
await login(page);
await apiCreateRepo(page.request, {name: repoName});
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects via UI
@@ -404,7 +404,7 @@ test('select projects on new issue page shows in sidebar', async ({page}) => {
const project2Title = 'Project Two';
await login(page);
await apiCreateRepo(page.request, {name: repoName});
await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects

View File

@@ -5,7 +5,7 @@ import {apiCreateRepo, apiCreateIssue, assertNoJsError, randomString} from './ut
test('mermaid diagram in issue', async ({page, request}) => {
const repoName = `e2e-mermaid-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await apiCreateRepo(request, {name: repoName});
await apiCreateRepo(request, {name: repoName, autoInit: false});
const body = '```mermaid\nflowchart LR\n Alpha --> Beta\n Beta --> Gamma\n```\n';
const {index} = await apiCreateIssue(request, {owner, repo: repoName, title: 'mermaid test', body});
await page.goto(`/${owner}/${repoName}/issues/${index}`);

View File

@@ -4,7 +4,7 @@ import {login, apiCreateRepo, randomString} from './utils.ts';
test('create a milestone', async ({page}) => {
const repoName = `e2e-milestone-${randomString(8)}`;
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName, autoInit: false})]);
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/milestones/new`);
await page.getByPlaceholder('Title').fill('Test Milestone');
await page.getByRole('button', {name: 'Create Milestone'}).click();

View File

@@ -0,0 +1,23 @@
import {env} from 'node:process';
import {test, expect} from '@playwright/test';
import {login, apiCreateRepo, apiCreateFile, randomString} from './utils.ts';
test('create a pull request from the compare page', async ({page, request}) => {
const repoName = `e2e-pr-create-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await apiCreateRepo(request, {name: repoName});
await Promise.all([
apiCreateFile(request, owner, repoName, 'feat.txt', 'feature content\n', {branch: 'main', newBranch: 'feat'}),
login(page),
]);
// expand=1 renders the PR form directly, skipping the "New Pull Request" toggle click
await page.goto(`/${owner}/${repoName}/compare/main...feat?expand=1`);
const title = `e2e-pr-${randomString(8)}`;
await page.getByPlaceholder('Title').fill(title);
await page.getByRole('button', {name: 'Create Pull Request'}).click();
// commit, not full load: the PR title heading is server-rendered, so the assertion can resolve before the heavy diff/timeline finishes
await page.waitForURL(new RegExp(`/${owner}/${repoName}/pulls/\\d+$`), {waitUntil: 'commit'});
await expect(page.getByRole('heading', {name: title})).toBeVisible();
});

View File

@@ -5,7 +5,7 @@ import {login, apiCreateRepo, apiCreateIssue, randomString} from './utils.ts';
test('toggle issue reactions', async ({page, request}) => {
const repoName = `e2e-reactions-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await apiCreateRepo(request, {name: repoName});
await apiCreateRepo(request, {name: repoName, autoInit: false});
await Promise.all([
apiCreateIssue(request, {owner, repo: repoName, title: 'Reaction test'}),
login(page),

19
tests/e2e/release.test.ts Normal file
View File

@@ -0,0 +1,19 @@
import {env} from 'node:process';
import {test, expect} from '@playwright/test';
import {login, apiCreateRepo, randomString} from './utils.ts';
test('create a release', async ({page, request}) => {
const repoName = `e2e-release-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]);
await page.goto(`/${owner}/${repoName}/releases/new`);
const tag = `v1.0.0-${randomString(8)}`;
const title = `e2e-release-${randomString(8)}`;
await page.getByLabel('Tag name').fill(tag);
await page.getByLabel('Release title').fill(title);
await page.getByRole('button', {name: 'Publish Release'}).click();
await page.waitForURL(new RegExp(`/${owner}/${repoName}/releases$`));
await expect(page.locator('.release-list-title')).toContainText(title);
});

View File

@@ -0,0 +1,20 @@
import {test, expect} from '@playwright/test';
import {login, apiCreateRepo, apiCreateUser, apiUserHeaders, randomString} from './utils.ts';
test('star and watch a repository', async ({page, request}) => {
const owner = `sw-owner-${randomString(8)}`;
const repoName = `e2e-star-watch-${randomString(8)}`;
await apiCreateUser(request, owner);
await Promise.all([
apiCreateRepo(request, {name: repoName, autoInit: false, headers: apiUserHeaders(owner)}),
login(page),
]);
await page.goto(`/${owner}/${repoName}`);
// exact match so "Star"/"Watch" don't also match "Unstar"/"Unwatch"
await page.getByRole('button', {name: 'Star', exact: true}).click();
await expect(page.getByRole('button', {name: 'Unstar'})).toBeVisible();
await page.getByRole('button', {name: 'Watch', exact: true}).click();
await expect(page.getByRole('button', {name: 'Unwatch'})).toBeVisible();
});