diff --git a/.gitignore b/.gitignore index fc210af692..79a4108dab 100644 --- a/.gitignore +++ b/.gitignore @@ -73,6 +73,7 @@ cpu.out /tests/e2e/reports /tests/e2e/test-artifacts /tests/e2e/test-snapshots +/tests/e2e/.auth /tests/*.ini /tests/**/*.git/**/*.sample /node_modules diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 8d8858bfd5..35fc5e7d1d 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -250,16 +250,18 @@ test('For anyone', async ({page}) => { If you need a user account, you can use something like: ~~~js -import {test, login_user, login} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); // or another user -}); +// reuse user2 token from scope `shared` +test.use({user: 'user2', authScope: 'shared'}) -test('For signed users only', async ({browser}, workerInfo) => { - const page = await login({browser}, workerInfo); +test('For signed users only', async ({page}) => { + +}) ~~~ +users are created in [utils_e2e_test.go](utils_e2e_test.go) + ### Run tests very selectively Browser testing can take some time. diff --git a/tests/e2e/actions.test.e2e.ts b/tests/e2e/actions.test.e2e.ts index a66b608080..6236fe70d3 100644 --- a/tests/e2e/actions.test.e2e.ts +++ b/tests/e2e/actions.test.e2e.ts @@ -10,72 +10,61 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts'; - -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +import {save_visual, test} from './utils_e2e.ts'; const workflow_trigger_notification_text = 'This workflow has a workflow_dispatch event trigger.'; +test.describe('Workflow Authenticated user2', () => { + test.use({user: 'user2'}); -test('workflow dispatch present', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); + test('workflow dispatch present', async ({page}) => { + await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); - await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); + await expect(page.getByText(workflow_trigger_notification_text)).toBeVisible(); - await expect(page.getByText(workflow_trigger_notification_text)).toBeVisible(); + const run_workflow_btn = page.locator('#workflow_dispatch_dropdown>button'); + await expect(run_workflow_btn).toBeVisible(); - const run_workflow_btn = page.locator('#workflow_dispatch_dropdown>button'); - await expect(run_workflow_btn).toBeVisible(); - - const menu = page.locator('#workflow_dispatch_dropdown>.menu'); - await expect(menu).toBeHidden(); - await run_workflow_btn.click(); - await expect(menu).toBeVisible(); - await save_visual(page); -}); - -test('workflow dispatch error: missing inputs', async ({browser}, workerInfo) => { - test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383'); - - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); - - await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); - - await page.locator('#workflow_dispatch_dropdown>button').click(); - - // Remove the required attribute so we can trigger the error message! - await page.evaluate(() => { - const elem = document.querySelector('input[name="inputs[string2]"]'); - elem?.removeAttribute('required'); + const menu = page.locator('#workflow_dispatch_dropdown>.menu'); + await expect(menu).toBeHidden(); + await run_workflow_btn.click(); + await expect(menu).toBeVisible(); + await save_visual(page); }); - await page.locator('#workflow-dispatch-submit').click(); + test('dispatch error: missing inputs', async ({page}, testInfo) => { + test.skip(testInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383'); - await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible(); - await save_visual(page); -}); + await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); -test('workflow dispatch success', async ({browser}, workerInfo) => { - test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383'); + await page.locator('#workflow_dispatch_dropdown>button').click(); - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); + // Remove the required attribute so we can trigger the error message! + await page.evaluate(() => { + const elem = document.querySelector('input[name="inputs[string2]"]'); + elem?.removeAttribute('required'); + }); - await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); + await page.locator('#workflow-dispatch-submit').click(); - await page.locator('#workflow_dispatch_dropdown>button').click(); + await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible(); + await save_visual(page); + }); - await page.fill('input[name="inputs[string2]"]', 'abc'); - await save_visual(page); - await page.locator('#workflow-dispatch-submit').click(); + test('dispatch success', async ({page}, testInfo) => { + test.skip(testInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383'); + await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); - await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible(); + await page.locator('#workflow_dispatch_dropdown>button').click(); - await expect(page.locator('.run-list>:first-child .run-list-meta', {hasText: 'now'})).toBeVisible(); - await save_visual(page); + await page.fill('input[name="inputs[string2]"]', 'abc'); + await save_visual(page); + await page.locator('#workflow-dispatch-submit').click(); + + await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible(); + + await expect(page.locator('.run-list>:first-child .run-list-meta', {hasText: 'now'})).toBeVisible(); + await save_visual(page); + }); }); test('workflow dispatch box not available for unauthenticated users', async ({page}) => { diff --git a/tests/e2e/dashboard-ci-status.test.e2e.ts b/tests/e2e/dashboard-ci-status.test.e2e.ts index 1d23122b44..800fc951e6 100644 --- a/tests/e2e/dashboard-ci-status.test.e2e.ts +++ b/tests/e2e/dashboard-ci-status.test.e2e.ts @@ -3,21 +3,24 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts'; +import {save_visual, test} from './utils_e2e.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +test.use({user: 'user2'}); -test('Correct link and tooltip', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); +test.describe.configure({retries: 2}); + +test('Correct link and tooltip', async ({page}, testInfo) => { + if (testInfo.retry) { + await page.goto('/user2/test_workflows/actions'); + } + + const searchResponse = page.waitForResponse((resp) => resp.url().includes('/repo/search?') && resp.status() === 200); const response = await page.goto('/?repo-search-query=test_workflows'); expect(response?.status()).toBe(200); + await searchResponse; + const repoStatus = page.locator('.dashboard-repos .repo-owner-name-list > li:nth-child(1) > a:nth-child(2)'); - // wait for network activity to cease (so status was loaded in frontend) - await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle await expect(repoStatus).toHaveAttribute('href', '/user2/test_workflows/actions', {timeout: 10000}); await expect(repoStatus).toHaveAttribute('data-tooltip-content', /^(Error|Failure)$/); await save_visual(page); diff --git a/tests/e2e/git-notes.test.e2e.ts b/tests/e2e/git-notes.test.e2e.ts index 8b80a3aa77..4245853b24 100644 --- a/tests/e2e/git-notes.test.e2e.ts +++ b/tests/e2e/git-notes.test.e2e.ts @@ -1,14 +1,10 @@ // @ts-check -import {test, expect} from '@playwright/test'; -import {login_user, save_visual, load_logged_in_context} from './utils_e2e.ts'; +import {expect} from '@playwright/test'; +import {save_visual, test} from './utils_e2e.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +test.use({user: 'user2'}); -test('Change git note', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); +test('Change git note', async ({page}) => { let response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d'); expect(response?.status()).toBe(200); diff --git a/tests/e2e/issue-comment.test.e2e.ts b/tests/e2e/issue-comment.test.e2e.ts index 4fce16764b..933e65fa32 100644 --- a/tests/e2e/issue-comment.test.e2e.ts +++ b/tests/e2e/issue-comment.test.e2e.ts @@ -5,14 +5,11 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual, login_user, login} from './utils_e2e.ts'; +import {test, save_visual} from './utils_e2e.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +test.use({user: 'user2'}); -test('Menu accessibility', async ({browser}, workerInfo) => { - const page = await login({browser}, workerInfo); +test('Menu accessibility', async ({page}) => { await page.goto('/user2/repo1/issues/1'); await expect(page.getByLabel('user2 reacted eyes. Remove eyes')).toBeVisible(); await expect(page.getByLabel('reacted laugh. Remove laugh')).toBeVisible(); @@ -24,9 +21,8 @@ test('Menu accessibility', async ({browser}, workerInfo) => { await expect(page.getByLabel('user1, user2 reacted laugh. Remove laugh')).toBeVisible(); }); -test('Hyperlink paste behaviour', async ({browser}, workerInfo) => { +test('Hyperlink paste behaviour', async ({page}, workerInfo) => { test.skip(['Mobile Safari', 'Mobile Chrome', 'webkit'].includes(workerInfo.project.name), 'Mobile clients seem to have very weird behaviour with this test, which I cannot confirm with real usage'); - const page = await login({browser}, workerInfo); await page.goto('/user2/repo1/issues/new'); await page.locator('textarea').click(); // same URL @@ -58,8 +54,7 @@ test('Hyperlink paste behaviour', async ({browser}, workerInfo) => { await page.locator('textarea').fill(''); }); -test('Always focus edit tab first on edit', async ({browser}, workerInfo) => { - const page = await login({browser}, workerInfo); +test('Always focus edit tab first on edit', async ({page}) => { const response = await page.goto('/user2/repo1/issues/1'); expect(response?.status()).toBe(200); @@ -82,9 +77,8 @@ test('Always focus edit tab first on edit', async ({browser}, workerInfo) => { await save_visual(page); }); -test('Quote reply', async ({browser}, workerInfo) => { +test('Quote reply', async ({page}, workerInfo) => { test.skip(workerInfo.project.name !== 'firefox', 'Uses Firefox specific selection quirks'); - const page = await login({browser}, workerInfo); const response = await page.goto('/user2/repo1/issues/1'); expect(response?.status()).toBe(200); @@ -157,9 +151,8 @@ test('Quote reply', async ({browser}, workerInfo) => { await editorTextarea.fill(''); }); -test('Pull quote reply', async ({browser}, workerInfo) => { +test('Pull quote reply', async ({page}, workerInfo) => { test.skip(workerInfo.project.name !== 'firefox', 'Uses Firefox specific selection quirks'); - const page = await login({browser}, workerInfo); const response = await page.goto('/user2/commitsonpr/pulls/1/files'); expect(response?.status()).toBe(200); diff --git a/tests/e2e/issue-sidebar.test.e2e.ts b/tests/e2e/issue-sidebar.test.e2e.ts index f4d50a13ba..fe2a6cec87 100644 --- a/tests/e2e/issue-sidebar.test.e2e.ts +++ b/tests/e2e/issue-sidebar.test.e2e.ts @@ -7,14 +7,13 @@ /* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["check_wip"] }] */ import {expect, type Page} from '@playwright/test'; -import {test, save_visual, login_user, login} from './utils_e2e.ts'; +import {save_visual, test} from './utils_e2e.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +test.use({user: 'user2'}); test.describe('Pull: Toggle WIP', () => { const prTitle = 'pull5'; + async function toggle_wip_to({page}, should: boolean) { await page.waitForLoadState('domcontentloaded'); if (should) { @@ -39,8 +38,7 @@ test.describe('Pull: Toggle WIP', () => { } } - test.beforeEach(async ({browser}, workerInfo) => { - const page = await login({browser}, workerInfo); + test.beforeEach(async ({page}) => { const response = await page.goto('/user2/repo1/pulls/5'); expect(response?.status()).toBe(200); // Status OK // ensure original title @@ -50,9 +48,8 @@ test.describe('Pull: Toggle WIP', () => { await check_wip({page}, false); }); - test('simple toggle', async ({browser}, workerInfo) => { + test('simple toggle', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); - const page = await login({browser}, workerInfo); await page.goto('/user2/repo1/pulls/5'); // toggle to WIP await toggle_wip_to({page}, true); @@ -62,9 +59,8 @@ test.describe('Pull: Toggle WIP', () => { await check_wip({page}, false); }); - test('manual edit', async ({browser}, workerInfo) => { + test('manual edit', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); - const page = await login({browser}, workerInfo); await page.goto('/user2/repo1/pulls/5'); // manually edit title to another prefix await page.locator('#issue-title-edit-show').click(); @@ -76,9 +72,8 @@ test.describe('Pull: Toggle WIP', () => { await check_wip({page}, false); }); - test('maximum title length', async ({browser}, workerInfo) => { + test('maximum title length', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); - const page = await login({browser}, workerInfo); await page.goto('/user2/repo1/pulls/5'); // check maximum title length is handled gracefully const maxLenStr = prTitle + 'a'.repeat(240); @@ -96,17 +91,16 @@ test.describe('Pull: Toggle WIP', () => { }); }); -test('Issue: Labels', async ({browser}, workerInfo) => { +test('Issue: Labels', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); - async function submitLabels({page}: {page: Page}) { + async function submitLabels({page}: { page: Page }) { const submitted = page.waitForResponse('/user2/repo1/issues/labels'); await page.locator('textarea').first().click(); // close via unrelated element await submitted; await page.waitForLoadState(); } - const page = await login({browser}, workerInfo); // select label list in sidebar only const labelList = page.locator('.issue-content-right .labels-list a'); const response = await page.goto('/user2/repo1/issues/1'); @@ -144,9 +138,8 @@ test('Issue: Labels', async ({browser}, workerInfo) => { await expect(labelList.filter({hasText: 'label1'})).toBeVisible(); }); -test('Issue: Assignees', async ({browser}, workerInfo) => { +test('Issue: Assignees', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); - const page = await login({browser}, workerInfo); // select label list in sidebar only const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item a'); @@ -182,9 +175,8 @@ test('Issue: Assignees', async ({browser}, workerInfo) => { await expect(page.locator('.ui.assignees.list .item.no-select')).toBeHidden(); }); -test('New Issue: Assignees', async ({browser}, workerInfo) => { +test('New Issue: Assignees', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); - const page = await login({browser}, workerInfo); // select label list in sidebar only const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item'); @@ -224,9 +216,8 @@ test('New Issue: Assignees', async ({browser}, workerInfo) => { await save_visual(page); }); -test('Issue: Milestone', async ({browser}, workerInfo) => { +test('Issue: Milestone', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); - const page = await login({browser}, workerInfo); const response = await page.goto('/user2/repo1/issues/1'); expect(response?.status()).toBe(200); @@ -248,9 +239,8 @@ test('Issue: Milestone', async ({browser}, workerInfo) => { await expect(page.locator('.timeline-item.event').last()).toContainText('user2 removed this from the milestone1 milestone'); }); -test('New Issue: Milestone', async ({browser}, workerInfo) => { +test('New Issue: Milestone', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); - const page = await login({browser}, workerInfo); const response = await page.goto('/user2/repo1/issues/new'); expect(response?.status()).toBe(200); diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts index ca2d6e01b6..762113d563 100644 --- a/tests/e2e/markdown-editor.test.e2e.ts +++ b/tests/e2e/markdown-editor.test.e2e.ts @@ -5,21 +5,16 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual, load_logged_in_context, login_user} from './utils_e2e.ts'; +import {save_visual, test} from './utils_e2e.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +test.use({user: 'user2'}); -test('Markdown image preview behaviour', async ({browser}, workerInfo) => { +test('Markdown image preview behaviour', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari;'); - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - // Editing the root README.md file for image preview const editPath = '/user2/repo1/src/branch/master/README.md'; - const page = await context.newPage(); const response = await page.goto(editPath, {waitUntil: 'domcontentloaded'}); expect(response?.status()).toBe(200); @@ -43,12 +38,9 @@ test('Markdown image preview behaviour', async ({browser}, workerInfo) => { await save_visual(page); }); -test('markdown indentation', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - +test('markdown indentation', async ({page}) => { const initText = `* first\n* second\n* third\n* last`; - const page = await context.newPage(); const response = await page.goto('/user2/repo1/issues/new'); expect(response?.status()).toBe(200); @@ -116,12 +108,9 @@ test('markdown indentation', async ({browser}, workerInfo) => { await expect(textarea).toHaveValue(initText); }); -test('markdown list continuation', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - +test('markdown list continuation', async ({page}) => { const initText = `* first\n* second\n* third\n* last`; - const page = await context.newPage(); const response = await page.goto('/user2/repo1/issues/new'); expect(response?.status()).toBe(200); @@ -213,10 +202,7 @@ test('markdown list continuation', async ({browser}, workerInfo) => { } }); -test('markdown insert table', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - - const page = await context.newPage(); +test('markdown insert table', async ({page}) => { const response = await page.goto('/user2/repo1/issues/new'); expect(response?.status()).toBe(200); diff --git a/tests/e2e/org-settings.test.e2e.ts b/tests/e2e/org-settings.test.e2e.ts index 22a8bc0e2d..df554e0674 100644 --- a/tests/e2e/org-settings.test.e2e.ts +++ b/tests/e2e/org-settings.test.e2e.ts @@ -5,16 +5,13 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual, login_user, login} from './utils_e2e.ts'; +import {save_visual, test} from './utils_e2e.ts'; import {validate_form} from './shared/forms.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +test.use({user: 'user2'}); -test('org team settings', async ({browser}, workerInfo) => { +test('org team settings', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Cannot get it to work - as usual'); - const page = await login({browser}, workerInfo); const response = await page.goto('/org/org3/teams/team1/edit'); expect(response?.status()).toBe(200); diff --git a/tests/e2e/profile_actions.test.e2e.ts b/tests/e2e/profile_actions.test.e2e.ts index 65090e62b2..a66dc43aab 100644 --- a/tests/e2e/profile_actions.test.e2e.ts +++ b/tests/e2e/profile_actions.test.e2e.ts @@ -5,13 +5,11 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts'; +import {save_visual, test} from './utils_e2e.ts'; -test('Follow actions', async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); +test.use({user: 'user2'}); +test('Follow actions', async ({page}) => { await page.goto('/user1'); // Check if following and then unfollowing works. diff --git a/tests/e2e/reaction-selectors.test.e2e.ts b/tests/e2e/reaction-selectors.test.e2e.ts index 3ce71b24d7..54b7d91869 100644 --- a/tests/e2e/reaction-selectors.test.e2e.ts +++ b/tests/e2e/reaction-selectors.test.e2e.ts @@ -4,11 +4,9 @@ // @watch end import {expect, type Locator} from '@playwright/test'; -import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts'; +import {save_visual, test} from './utils_e2e.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +test.use({user: 'user2'}); const assertReactionCounts = (comment: Locator, counts: unknown) => expect(async () => { @@ -26,6 +24,7 @@ const assertReactionCounts = (comment: Locator, counts: unknown) => ]), ), ); + // eslint-disable-next-line playwright/no-standalone-expect return expect(reactions).toStrictEqual(counts); }).toPass(); @@ -35,10 +34,7 @@ async function toggleReaction(menu: Locator, reaction: string) { await menu.locator(`[role=menuitem][data-reaction-content="${reaction}"]`).click(); } -test('Reaction Selectors', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); - +test('Reaction Selectors', async ({page}) => { const response = await page.goto('/user2/repo1/issues/1'); expect(response?.status()).toBe(200); diff --git a/tests/e2e/release.test.e2e.ts b/tests/e2e/release.test.e2e.ts index fefa446c59..49c67793e6 100644 --- a/tests/e2e/release.test.e2e.ts +++ b/tests/e2e/release.test.e2e.ts @@ -9,24 +9,18 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts'; +import {save_visual, test} from './utils_e2e.ts'; import {validate_form} from './shared/forms.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +test.use({user: 'user2'}); test.describe.configure({ timeout: 30000, }); -test('External Release Attachments', async ({browser, isMobile}, workerInfo) => { +test('External Release Attachments', async ({page, isMobile}) => { test.skip(isMobile); - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - /** @type {import('@playwright/test').Page} */ - const page = await context.newPage(); - // Click "New Release" await page.goto('/user2/repo2/releases'); await page.click('.button.small.primary'); diff --git a/tests/e2e/repo-code.test.e2e.ts b/tests/e2e/repo-code.test.e2e.ts index 264dd3a8e0..335fb5b7f5 100644 --- a/tests/e2e/repo-code.test.e2e.ts +++ b/tests/e2e/repo-code.test.e2e.ts @@ -5,13 +5,9 @@ // @watch end import {expect, type Page} from '@playwright/test'; -import {test, save_visual, login_user, login} from './utils_e2e.ts'; +import {save_visual, test} from './utils_e2e.ts'; import {accessibilityCheck} from './shared/accessibility.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); - async function assertSelectedLines(page: Page, nums: string[]) { const pageAssertions = async () => { expect( @@ -81,20 +77,23 @@ test('Readable diff', async ({page}, workerInfo) => { } }); -test('Username highlighted in commits', async ({browser}, workerInfo) => { - const page = await login({browser}, workerInfo); - await page.goto('/user2/mentions-highlighted/commits/branch/main'); - // check first commit - await page.getByRole('link', {name: 'A commit message which'}).click(); - await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/); - await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); - await accessibilityCheck({page}, ['.commit-header'], [], []); - await save_visual(page); - // check second commit - await page.goto('/user2/mentions-highlighted/commits/branch/main'); - await page.locator('tbody').getByRole('link', {name: 'Another commit which mentions'}).click(); - await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/); - await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); - await accessibilityCheck({page}, ['.commit-header'], [], []); - await save_visual(page); +test.describe('As authenticated user', () => { + test.use({user: 'user2'}); + + test('Username highlighted in commits', async ({page}) => { + await page.goto('/user2/mentions-highlighted/commits/branch/main'); + // check first commit + await page.getByRole('link', {name: 'A commit message which'}).click(); + await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/); + await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + await accessibilityCheck({page}, ['.commit-header'], [], []); + await save_visual(page); + // check second commit + await page.goto('/user2/mentions-highlighted/commits/branch/main'); + await page.locator('tbody').getByRole('link', {name: 'Another commit which mentions'}).click(); + await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/); + await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + await accessibilityCheck({page}, ['.commit-header'], [], []); + await save_visual(page); + }); }); diff --git a/tests/e2e/repo-migrate.test.e2e.ts b/tests/e2e/repo-migrate.test.e2e.ts index a0f9ab6c80..428c2cb171 100644 --- a/tests/e2e/repo-migrate.test.e2e.ts +++ b/tests/e2e/repo-migrate.test.e2e.ts @@ -3,15 +3,13 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts'; +import {test, save_visual, test_context} from './utils_e2e.ts'; -test.beforeAll(({browser}, workerInfo) => login_user(browser, workerInfo, 'user2')); +test.use({user: 'user2'}); -test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo) => { +test('Migration Progress Page', async ({page, browser}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky actionability checks on Mobile Safari'); - const page = await (await load_logged_in_context(browser, workerInfo, 'user2')).newPage(); - expect((await page.goto('/user2/invalidrepo'))?.status(), 'repo should not exist yet').toBe(404); await page.goto('/repo/migrate?service_type=1'); @@ -23,10 +21,12 @@ test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo await form.locator('button.primary').click({timeout: 5000}); await expect(page).toHaveURL('user2/invalidrepo'); await save_visual(page); - // page screenshot of unauthedPage is checked automatically after the test + // page screenshot of unauthenticatedPage is checked automatically after the test - expect((await unauthedPage.goto('/user2/invalidrepo'))?.status(), 'public migration page should be accessible').toBe(200); - await expect(unauthedPage.locator('#repo_migrating_progress')).toBeVisible(); + const ctx = await test_context(browser); + const unauthenticatedPage = await ctx.newPage(); + expect((await unauthenticatedPage.goto('/user2/invalidrepo'))?.status(), 'public migration page should be accessible').toBe(200); + await expect(unauthenticatedPage.locator('#repo_migrating_progress')).toBeVisible(); await page.reload(); await expect(page.locator('#repo_migrating_failed')).toBeVisible(); diff --git a/tests/e2e/repo-new.test.e2e.ts b/tests/e2e/repo-new.test.e2e.ts index c9cc29ad56..ad202825a0 100644 --- a/tests/e2e/repo-new.test.e2e.ts +++ b/tests/e2e/repo-new.test.e2e.ts @@ -4,15 +4,12 @@ // @watch end import {expect} from '@playwright/test'; -import {test, dynamic_id, save_visual, login_user, login} from './utils_e2e.ts'; +import {test, dynamic_id, save_visual} from './utils_e2e.ts'; import {validate_form} from './shared/forms.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +test.use({user: 'user2'}); -test('New repo: invalid', async ({browser}, workerInfo) => { - const page = await login({browser}, workerInfo); +test('New repo: invalid', async ({page}) => { const response = await page.goto('/repo/create'); expect(response?.status()).toBe(200); // check that relevant form content is hidden or available @@ -28,8 +25,7 @@ test('New repo: invalid', async ({browser}, workerInfo) => { await save_visual(page); }); -test('New repo: initialize', async ({browser}, workerInfo) => { - const page = await login({browser}, workerInfo); +test('New repo: initialize', async ({page}, workerInfo) => { const response = await page.goto('/repo/create'); expect(response?.status()).toBe(200); // check that relevant form content is hidden or available @@ -62,8 +58,7 @@ test('New repo: initialize', async ({browser}, workerInfo) => { await save_visual(page); }); -test('New repo: initialize later', async ({browser}, workerInfo) => { - const page = await login({browser}, workerInfo); +test('New repo: initialize later', async ({page}) => { const response = await page.goto('/repo/create'); expect(response?.status()).toBe(200); @@ -97,9 +92,8 @@ test('New repo: initialize later', async ({browser}, workerInfo) => { await save_visual(page); }); -test('New repo: from template', async ({browser}, workerInfo) => { +test('New repo: from template', async ({page}, workerInfo) => { test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'WebKit browsers seem to have CORS issues with localhost here.'); - const page = await login({browser}, workerInfo); const response = await page.goto('/repo/create'); expect(response?.status()).toBe(200); @@ -114,8 +108,7 @@ test('New repo: from template', async ({browser}, workerInfo) => { await save_visual(page); }); -test('New repo: label set', async ({browser}, workerInfo) => { - const page = await login({browser}, workerInfo); +test('New repo: label set', async ({page}) => { await page.goto('/repo/create'); const reponame = dynamic_id(); diff --git a/tests/e2e/repo-settings.test.e2e.ts b/tests/e2e/repo-settings.test.e2e.ts index 113b15181b..3d260866fb 100644 --- a/tests/e2e/repo-settings.test.e2e.ts +++ b/tests/e2e/repo-settings.test.e2e.ts @@ -7,16 +7,13 @@ // @watch end import {expect} from '@playwright/test'; -import {test, save_visual, login_user, login} from './utils_e2e.ts'; +import {test, save_visual} from './utils_e2e.ts'; import {validate_form} from './shared/forms.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +test.use({user: 'user2'}); -test('repo webhook settings', async ({browser}, workerInfo) => { +test('repo webhook settings', async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Cannot get it to work - as usual'); - const page = await login({browser}, workerInfo); const response = await page.goto('/user2/repo1/settings/hooks/forgejo/new'); expect(response?.status()).toBe(200); @@ -35,9 +32,8 @@ test('repo webhook settings', async ({browser}, workerInfo) => { }); test.describe('repo branch protection settings', () => { - test('form', async ({browser}, workerInfo) => { - test.skip(workerInfo.project.name === 'Mobile Safari', 'Cannot get it to work - as usual'); - const page = await login({browser}, workerInfo); + test('form', async ({page}, {project}) => { + test.skip(project.name === 'Mobile Safari', 'Cannot get it to work - as usual'); const response = await page.goto('/user2/repo1/settings/branches/edit'); expect(response?.status()).toBe(200); @@ -56,8 +52,7 @@ test.describe('repo branch protection settings', () => { await save_visual(page); }); - test.afterEach(async ({browser}, workerInfo) => { - const page = await login({browser}, workerInfo); + test.afterEach(async ({page}) => { // delete the rule for the next test await page.goto('/user2/repo1/settings/branches/'); await page.waitForLoadState('domcontentloaded'); diff --git a/tests/e2e/right-settings-button.test.e2e.ts b/tests/e2e/right-settings-button.test.e2e.ts index bfb1800a27..e1c40fdd4d 100644 --- a/tests/e2e/right-settings-button.test.e2e.ts +++ b/tests/e2e/right-settings-button.test.e2e.ts @@ -5,19 +5,12 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, load_logged_in_context} from './utils_e2e.ts'; +import {test} from './utils_e2e.ts'; -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); - -test.describe('desktop viewport', () => { - test.use({viewport: {width: 1920, height: 300}}); - - test('Settings button on right of repo header', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); +test.describe('desktop viewport as user 2', () => { + test.use({user: 'user2', viewport: {width: 1920, height: 300}}); + test('Settings button on right of repo header', async ({page}) => { await page.goto('/user2/repo1'); const settingsBtn = page.locator('.overflow-menu-items>#settings-btn'); @@ -27,24 +20,7 @@ test.describe('desktop viewport', () => { await expect(page.locator('.overflow-menu-button')).toHaveCount(0); }); - test('Settings button on right of repo header also when add more button is shown', async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user12'); - const context = await load_logged_in_context(browser, workerInfo, 'user12'); - const page = await context.newPage(); - - await page.goto('/user12/repo10'); - - const settingsBtn = page.locator('.overflow-menu-items>#settings-btn'); - await expect(settingsBtn).toBeVisible(); - await expect(settingsBtn).toHaveClass(/right/); - - await expect(page.locator('.overflow-menu-button')).toHaveCount(0); - }); - - test('Settings button on right of org header', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); - + test('Settings button on right of org header', async ({page}) => { await page.goto('/org3'); const settingsBtn = page.locator('.overflow-menu-items>#settings-btn'); @@ -53,6 +29,24 @@ test.describe('desktop viewport', () => { await expect(page.locator('.overflow-menu-button')).toHaveCount(0); }); +}); + +test.describe('desktop viewport as user12', () => { + test.use({user: 'user12', viewport: {width: 1920, height: 300}}); + + test('Settings button on right of repo header also when add more button is shown', async ({page}) => { + await page.goto('/user12/repo10'); + + const settingsBtn = page.locator('.overflow-menu-items>#settings-btn'); + await expect(settingsBtn).toBeVisible(); + await expect(settingsBtn).toHaveClass(/right/); + + await expect(page.locator('.overflow-menu-button')).toHaveCount(0); + }); +}); + +test.describe('desktop viewport, unauthenticated', () => { + test.use({viewport: {width: 1920, height: 300}}); test('User overview overflow menu should not be influenced', async ({page}) => { await page.goto('/user2'); @@ -64,12 +58,9 @@ test.describe('desktop viewport', () => { }); test.describe('small viewport', () => { - test.use({viewport: {width: 800, height: 300}}); - - test('Settings button in overflow menu of repo header', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); + test.use({user: 'user2', viewport: {width: 800, height: 300}}); + test('Settings button in overflow menu of repo header', async ({page}) => { await page.goto('/user2/repo1'); await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0); @@ -89,10 +80,7 @@ test.describe('small viewport', () => { expect(Array.from(new Set(items))).toHaveLength(items.length); }); - test('Settings button in overflow menu of org header', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); - + test('Settings button in overflow menu of org header', async ({page}) => { await page.goto('/org3'); await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0); @@ -111,6 +99,10 @@ test.describe('small viewport', () => { const items = shownItems.concat(overflowItems); expect(Array.from(new Set(items))).toHaveLength(items.length); }); +}); + +test.describe('small viewport, unauthenticated', () => { + test.use({viewport: {width: 800, height: 300}}); test('User overview overflow menu should not be influenced', async ({page}) => { await page.goto('/user2'); diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts index 7e25441ea3..80412e437d 100644 --- a/tests/e2e/utils_e2e.ts +++ b/tests/e2e/utils_e2e.ts @@ -1,9 +1,31 @@ import {expect, test as baseTest, type Browser, type BrowserContextOptions, type APIRequestContext, type TestInfo, type Page} from '@playwright/test'; -export const test = baseTest.extend({ - context: async ({browser}, use) => { - return use(await test_context(browser)); +import * as path from 'node:path'; + +const AUTH_PATH = 'tests/e2e/.auth'; + +type AuthScope = 'logout' | 'shared' | 'webauthn'; + +export type TestOptions = { + forEachTest: void + user: string | null; + authScope: AuthScope; +}; + +export const test = baseTest.extend({ + context: async ({browser, user, authScope, contextOptions}, use, {project}) => { + if (user && authScope) { + const browserName = project.name.toLowerCase().replace(' ', '-'); + contextOptions.storageState = path.join(AUTH_PATH, `state-${browserName}-${user}-${authScope}.json`); + } else { + // if no user is given, ensure to have clean state + contextOptions.storageState = {cookies: [], origins: []}; + } + + return use(await test_context(browser, contextOptions)); }, + user: null, + authScope: 'shared', // see https://playwright.dev/docs/test-fixtures#adding-global-beforeeachaftereach-hooks forEachTest: [async ({page}, use) => { await use(); @@ -15,7 +37,7 @@ export const test = baseTest.extend({ }, {auto: true}], }); -async function test_context(browser: Browser, options?: BrowserContextOptions) { +export async function test_context(browser: Browser, options?: BrowserContextOptions) { const context = await browser.newContext(options); context.on('page', (page) => { diff --git a/tests/e2e/utils_e2e_test.go b/tests/e2e/utils_e2e_test.go index bf1a8a418c..96fd905363 100644 --- a/tests/e2e/utils_e2e_test.go +++ b/tests/e2e/utils_e2e_test.go @@ -5,17 +5,27 @@ package e2e import ( "context" + "crypto/rand" + "encoding/hex" + "fmt" "net" "net/http" "net/url" "os" + "path/filepath" "regexp" + "strings" "testing" "time" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + modules_session "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/tests" + "code.forgejo.org/go-chi/session" "github.com/stretchr/testify/require" ) @@ -25,6 +35,8 @@ func onForgejoRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare . if len(prepare) == 0 || prepare[0] { defer tests.PrepareTestEnv(t, 1)() } + createSessions(t) + s := http.Server{ Handler: testE2eWebRoutes, } @@ -64,3 +76,118 @@ func onForgejoRun(t *testing.T, callback func(*testing.T, *url.URL), prepare ... callback(t.(*testing.T), u) }, prepare...) } + +func createSessions(t testing.TB) { + t.Helper() + // copied from playwright.config.ts + browsers := []string{ + "chromium", + "firefox", + "webkit", + "Mobile Chrome", + "Mobile Safari", + } + scopes := []string{ + "shared", + } + users := []string{ + "user1", + "user2", + "user12", + "user40", + } + + authState := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", ".auth") + err := os.RemoveAll(authState) + require.NoError(t, err) + + err = os.MkdirAll(authState, os.ModePerm) + require.NoError(t, err) + + createSessionCookie := stateHelper(t) + + for _, user := range users { + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: strings.ToLower(user)}) + for _, browser := range browsers { + for _, scope := range scopes { + stateFile := strings.ReplaceAll(strings.ToLower(fmt.Sprintf("state-%s-%s-%s.json", browser, user, scope)), " ", "-") + createSessionCookie(filepath.Join(authState, stateFile), u) + } + } + } +} + +func stateHelper(t testing.TB) func(stateFile string, user *user_model.User) { + type Cookie struct { + Name string `json:"name"` + Value string `json:"value"` + Domain string `json:"domain"` + Path string `json:"path"` + Expires int `json:"expires"` + HTTPOnly bool `json:"httpOnly"` + Secure bool `json:"secure"` + SameSite string `json:"sameSite"` + } + + type BrowserState struct { + Cookies []Cookie `json:"cookies"` + Origins []string `json:"origins"` + } + + options := session.Options{ + Provider: setting.SessionConfig.Provider, + ProviderConfig: setting.SessionConfig.ProviderConfig, + CookieName: setting.SessionConfig.CookieName, + CookiePath: setting.SessionConfig.CookiePath, + Gclifetime: setting.SessionConfig.Gclifetime, + Maxlifetime: setting.SessionConfig.Maxlifetime, + Secure: setting.SessionConfig.Secure, + SameSite: setting.SessionConfig.SameSite, + Domain: setting.SessionConfig.Domain, + } + + opt := session.PrepareOptions([]session.Options{options}) + + vsp := modules_session.VirtualSessionProvider{} + err := vsp.Init(opt.Maxlifetime, opt.ProviderConfig) + require.NoError(t, err) + + return func(stateFile string, user *user_model.User) { + buf := make([]byte, opt.IDLength/2) + _, err = rand.Read(buf) + require.NoError(t, err) + + sessionID := hex.EncodeToString(buf) + + s, err := vsp.Read(sessionID) + require.NoError(t, err) + + err = s.Set("uid", user.ID) + require.NoError(t, err) + + err = s.Release() + require.NoError(t, err) + + state := BrowserState{ + Cookies: []Cookie{ + { + Name: opt.CookieName, + Value: sessionID, + Domain: setting.Domain, + Path: "/", + Expires: -1, + HTTPOnly: true, + Secure: false, + SameSite: "Lax", + }, + }, + Origins: []string{}, + } + + jsonData, err := json.Marshal(state) + require.NoError(t, err) + + err = os.WriteFile(stateFile, jsonData, 0o644) + require.NoError(t, err) + } +}