chore(e2e): simplify authentication setup (#6400)
Some checks are pending
/ release (push) Waiting to run
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions

Replaced manual login and context loading across tests with Playwright's `test.use` configuration for user authentication. This simplifies test setup, improves readability, and reduces repetition.

For #6362

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6400
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Julian Schlarb <julian.schlarb@denktmit.de>
Co-committed-by: Julian Schlarb <julian.schlarb@denktmit.de>
This commit is contained in:
Julian Schlarb 2025-01-05 05:17:04 +00:00 committed by Gusted
parent a2eb249766
commit 68d690b6b9
19 changed files with 327 additions and 254 deletions

1
.gitignore vendored
View file

@ -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

View file

@ -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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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