From 7c12446c1f14d91ca9118ff1a98a81e9bd9ed684 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 22 May 2026 20:52:04 +0200 Subject: [PATCH] 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) Co-authored-by: Nicolas --- tests/e2e/events.test.ts | 4 ++-- tests/e2e/fork.test.ts | 18 ++++++++++++++++++ tests/e2e/issue-comment.test.ts | 24 ++++++++++++++++++++++++ tests/e2e/issue-project.test.ts | 18 +++++++++--------- tests/e2e/mermaid.test.ts | 2 +- tests/e2e/milestone.test.ts | 2 +- tests/e2e/pr-create.test.ts | 23 +++++++++++++++++++++++ tests/e2e/reactions.test.ts | 2 +- tests/e2e/release.test.ts | 19 +++++++++++++++++++ tests/e2e/repo-star-watch.test.ts | 20 ++++++++++++++++++++ 10 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 tests/e2e/fork.test.ts create mode 100644 tests/e2e/issue-comment.test.ts create mode 100644 tests/e2e/pr-create.test.ts create mode 100644 tests/e2e/release.test.ts create mode 100644 tests/e2e/repo-star-watch.test.ts diff --git a/tests/e2e/events.test.ts b/tests/e2e/events.test.ts index c7cfef6e9c2..54b4774a5cc 100644 --- a/tests/e2e/events.test.ts +++ b/tests/e2e/events.test.ts @@ -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}); })(), diff --git a/tests/e2e/fork.test.ts b/tests/e2e/fork.test.ts new file mode 100644 index 00000000000..77ccda1242e --- /dev/null +++ b/tests/e2e/fork.test.ts @@ -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(); +}); diff --git a/tests/e2e/issue-comment.test.ts b/tests/e2e/issue-comment.test.ts new file mode 100644 index 00000000000..d3de59ba5f6 --- /dev/null +++ b/tests/e2e/issue-comment.test.ts @@ -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(); +}); diff --git a/tests/e2e/issue-project.test.ts b/tests/e2e/issue-project.test.ts index b1fed72a6f9..1595cfadc5a 100644 --- a/tests/e2e/issue-project.test.ts +++ b/tests/e2e/issue-project.test.ts @@ -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 diff --git a/tests/e2e/mermaid.test.ts b/tests/e2e/mermaid.test.ts index b2e066e5510..1523577eaa7 100644 --- a/tests/e2e/mermaid.test.ts +++ b/tests/e2e/mermaid.test.ts @@ -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}`); diff --git a/tests/e2e/milestone.test.ts b/tests/e2e/milestone.test.ts index 5a688fb1282..106381e64f0 100644 --- a/tests/e2e/milestone.test.ts +++ b/tests/e2e/milestone.test.ts @@ -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(); diff --git a/tests/e2e/pr-create.test.ts b/tests/e2e/pr-create.test.ts new file mode 100644 index 00000000000..592512683fc --- /dev/null +++ b/tests/e2e/pr-create.test.ts @@ -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(); +}); diff --git a/tests/e2e/reactions.test.ts b/tests/e2e/reactions.test.ts index 2048b938cf2..a6b61837416 100644 --- a/tests/e2e/reactions.test.ts +++ b/tests/e2e/reactions.test.ts @@ -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), diff --git a/tests/e2e/release.test.ts b/tests/e2e/release.test.ts new file mode 100644 index 00000000000..219a16d2c5f --- /dev/null +++ b/tests/e2e/release.test.ts @@ -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); +}); diff --git a/tests/e2e/repo-star-watch.test.ts b/tests/e2e/repo-star-watch.test.ts new file mode 100644 index 00000000000..d5f84491651 --- /dev/null +++ b/tests/e2e/repo-star-watch.test.ts @@ -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(); +});