mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-22 12:54:53 -05:00
Merge pull request 'Regular visual regression testing' (#6117) from fnetx/playwright-visual into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6117 Reviewed-by: 0ko <0ko@noreply.codeberg.org> Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
This commit is contained in:
commit
f5cfdd80a7
18 changed files with 117 additions and 44 deletions
|
@ -103,4 +103,5 @@ export default {
|
||||||
outputDir: 'tests/e2e/test-artifacts/',
|
outputDir: 'tests/e2e/test-artifacts/',
|
||||||
/* Folder for explicit snapshots for visual testing */
|
/* Folder for explicit snapshots for visual testing */
|
||||||
snapshotDir: 'tests/e2e/test-snapshots/',
|
snapshotDir: 'tests/e2e/test-snapshots/',
|
||||||
|
snapshotPathTemplate: '{snapshotDir}/snapshots/{testFilePath}/{projectName}_{arg}{ext}',
|
||||||
} satisfies PlaywrightTestConfig;
|
} satisfies PlaywrightTestConfig;
|
||||||
|
|
|
@ -155,20 +155,6 @@ For SQLite:
|
||||||
make test-e2e-sqlite#example
|
make test-e2e-sqlite#example
|
||||||
```
|
```
|
||||||
|
|
||||||
### Visual testing
|
|
||||||
|
|
||||||
> **Warning**
|
|
||||||
> This is not currently used by most Forgejo contributors.
|
|
||||||
> Your help to improve the situation and allow for visual testing is appreciated.
|
|
||||||
|
|
||||||
Although the main goal of e2e is assertion testing, we have added a framework for visual regression testing. If you are working on front-end features, please use the following:
|
|
||||||
- Check out `main`, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1` to generate outputs. This will initially fail, as no screenshots exist. You can run the e2e tests again to assert that it passes.
|
|
||||||
- Check out your branch, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1`. You should be able to assert that your front-end changes don't break any other tests unintentionally.
|
|
||||||
|
|
||||||
`VISUAL_TEST=1` will create screenshots in tests/e2e/test-snapshots. The test will fail the first time this is enabled (until we get visual test image persistence figured out), because it will be testing against an empty screenshot folder.
|
|
||||||
|
|
||||||
`ACCEPT_VISUAL=1` will overwrite the snapshot images with new images.
|
|
||||||
|
|
||||||
|
|
||||||
## Tips and tricks
|
## Tips and tricks
|
||||||
|
|
||||||
|
@ -216,6 +202,41 @@ you can alternatively use:
|
||||||
await page.waitForURL('**/target.html');
|
await page.waitForURL('**/target.html');
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
|
### Visual testing
|
||||||
|
|
||||||
|
Due to size and frequent updates, we do not host screenshots in the Forgejo repository.
|
||||||
|
However, it is good practice to ensure that your test is capable of generating relevant and stable screenshots.
|
||||||
|
Forgejo is regularly tested against visual regressions in a dedicated repository which contains the screenshots:
|
||||||
|
https://code.forgejo.org/forgejo/visual-browser-testing/
|
||||||
|
|
||||||
|
For tests that consume only the `page`,
|
||||||
|
screenshots are automatically created at the end of each test.
|
||||||
|
|
||||||
|
If your test visits different relevant screens or pages during the test,
|
||||||
|
or creates a custom `page` from context
|
||||||
|
(e.g. for tests that require a signed-in user)
|
||||||
|
calling `await save_visual(page);` explicitly in relevant positions is encouraged.
|
||||||
|
|
||||||
|
Please confirm locally that your screenshots are stable by performing several runs of your test.
|
||||||
|
When screenshots are available and reproducible,
|
||||||
|
check in your test without the screenshots.
|
||||||
|
|
||||||
|
When your screenshots differ between runs,
|
||||||
|
for example because dynamic elements (e.g. timestamps, commit hashes etc)
|
||||||
|
change between runs,
|
||||||
|
mask these elements in the `save_visual` function in `utils_e2e.ts`.
|
||||||
|
|
||||||
|
#### Working with screenshots
|
||||||
|
|
||||||
|
The following environment variables control visual testing:
|
||||||
|
|
||||||
|
`VISUAL_TEST=1` will create screenshots in tests/e2e/test-snapshots.
|
||||||
|
The test will fail the first time,
|
||||||
|
because the screenshots are not included with Forgejo.
|
||||||
|
Subsequent runs will comopare against your local copy of the screenshots.
|
||||||
|
|
||||||
|
`ACCEPT_VISUAL=1` will overwrite the snapshot images with new images.
|
||||||
|
|
||||||
### Only sign in if necessary
|
### Only sign in if necessary
|
||||||
|
|
||||||
Signing in takes time and is actually executed step-by-step.
|
Signing in takes time and is actually executed step-by-step.
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
import {test, login_user, load_logged_in_context} from './utils_e2e.ts';
|
import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
await login_user(browser, workerInfo, 'user2');
|
await login_user(browser, workerInfo, 'user2');
|
||||||
|
@ -33,6 +33,7 @@ test('workflow dispatch present', async ({browser}, workerInfo) => {
|
||||||
await expect(menu).toBeHidden();
|
await expect(menu).toBeHidden();
|
||||||
await run_workflow_btn.click();
|
await run_workflow_btn.click();
|
||||||
await expect(menu).toBeVisible();
|
await expect(menu).toBeVisible();
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('workflow dispatch error: missing inputs', async ({browser}, workerInfo) => {
|
test('workflow dispatch error: missing inputs', async ({browser}, workerInfo) => {
|
||||||
|
@ -54,6 +55,7 @@ test('workflow dispatch error: missing inputs', async ({browser}, workerInfo) =>
|
||||||
await page.locator('#workflow-dispatch-submit').click();
|
await page.locator('#workflow-dispatch-submit').click();
|
||||||
|
|
||||||
await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible();
|
await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible();
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('workflow dispatch success', async ({browser}, workerInfo) => {
|
test('workflow dispatch success', async ({browser}, workerInfo) => {
|
||||||
|
@ -67,11 +69,13 @@ test('workflow dispatch success', async ({browser}, workerInfo) => {
|
||||||
await page.locator('#workflow_dispatch_dropdown>button').click();
|
await page.locator('#workflow_dispatch_dropdown>button').click();
|
||||||
|
|
||||||
await page.fill('input[name="inputs[string2]"]', 'abc');
|
await page.fill('input[name="inputs[string2]"]', 'abc');
|
||||||
|
await save_visual(page);
|
||||||
await page.locator('#workflow-dispatch-submit').click();
|
await page.locator('#workflow-dispatch-submit').click();
|
||||||
|
|
||||||
await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible();
|
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 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}) => {
|
test('workflow dispatch box not available for unauthenticated users', async ({page}) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
import {test, login_user, load_logged_in_context} from './utils_e2e.ts';
|
import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
await login_user(browser, workerInfo, 'user2');
|
await login_user(browser, workerInfo, 'user2');
|
||||||
|
@ -20,4 +20,5 @@ test('Correct link and tooltip', async ({browser}, workerInfo) => {
|
||||||
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
|
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('href', '/user2/test_workflows/actions', {timeout: 10000});
|
||||||
await expect(repoStatus).toHaveAttribute('data-tooltip-content', /^(Error|Failure)$/);
|
await expect(repoStatus).toHaveAttribute('data-tooltip-content', /^(Error|Failure)$/);
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,11 +5,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
import {test, login_user, save_visual} from './utils_e2e.ts';
|
import {test} from './utils_e2e.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
|
||||||
await login_user(browser, workerInfo, 'user2');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Load Homepage', async ({page}) => {
|
test('Load Homepage', async ({page}) => {
|
||||||
const response = await page.goto('/');
|
const response = await page.goto('/');
|
||||||
|
@ -30,8 +26,6 @@ test('Register Form', async ({page}, workerInfo) => {
|
||||||
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
|
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||||
await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible();
|
await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible();
|
||||||
await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!');
|
await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!');
|
||||||
|
|
||||||
save_visual(page);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
// eslint-disable-next-line playwright/no-skipped-test
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import {test, expect} from '@playwright/test';
|
import {test, expect} from '@playwright/test';
|
||||||
import {login_user, load_logged_in_context} from './utils_e2e.ts';
|
import {login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
await login_user(browser, workerInfo, 'user2');
|
await login_user(browser, workerInfo, 'user2');
|
||||||
|
@ -17,14 +17,15 @@ test('Change git note', async ({browser}, workerInfo) => {
|
||||||
let textarea = page.locator('textarea[name="notes"]');
|
let textarea = page.locator('textarea[name="notes"]');
|
||||||
await expect(textarea).toBeVisible();
|
await expect(textarea).toBeVisible();
|
||||||
await textarea.fill('This is a new note');
|
await textarea.fill('This is a new note');
|
||||||
|
await save_visual(page);
|
||||||
|
|
||||||
await page.locator('#notes-save-button').click();
|
await page.locator('#notes-save-button').click();
|
||||||
|
await save_visual(page);
|
||||||
expect(response?.status()).toBe(200);
|
|
||||||
|
|
||||||
response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
|
response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
|
||||||
expect(response?.status()).toBe(200);
|
expect(response?.status()).toBe(200);
|
||||||
|
|
||||||
textarea = page.locator('textarea[name="notes"]');
|
textarea = page.locator('textarea[name="notes"]');
|
||||||
await expect(textarea).toHaveText('This is a new note');
|
await expect(textarea).toHaveText('This is a new note');
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
import {test, login_user, login} from './utils_e2e.ts';
|
import {test, save_visual, login_user, login} from './utils_e2e.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
await login_user(browser, workerInfo, 'user2');
|
await login_user(browser, workerInfo, 'user2');
|
||||||
|
@ -66,6 +66,7 @@ test('Always focus edit tab first on edit', async ({browser}, workerInfo) => {
|
||||||
|
|
||||||
await expect(editTab).toHaveClass(/active/);
|
await expect(editTab).toHaveClass(/active/);
|
||||||
await expect(previewTab).not.toHaveClass(/active/);
|
await expect(previewTab).not.toHaveClass(/active/);
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Quote reply', async ({browser}, workerInfo) => {
|
test('Quote reply', async ({browser}, workerInfo) => {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["check_wip"] }] */
|
/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["check_wip"] }] */
|
||||||
|
|
||||||
import {expect, type Page} from '@playwright/test';
|
import {expect, type Page} from '@playwright/test';
|
||||||
import {test, login_user, login} from './utils_e2e.ts';
|
import {test, save_visual, login_user, login} from './utils_e2e.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
await login_user(browser, workerInfo, 'user2');
|
await login_user(browser, workerInfo, 'user2');
|
||||||
|
@ -203,6 +203,7 @@ test('New Issue: Assignees', async ({browser}, workerInfo) => {
|
||||||
await page.locator('.select-assignees .menu .item').filter({hasText: 'user4'}).click();
|
await page.locator('.select-assignees .menu .item').filter({hasText: 'user4'}).click();
|
||||||
await page.locator('.select-assignees.dropdown').click();
|
await page.locator('.select-assignees.dropdown').click();
|
||||||
await expect(assigneesList.filter({hasText: 'user4'})).toBeVisible();
|
await expect(assigneesList.filter({hasText: 'user4'})).toBeVisible();
|
||||||
|
await save_visual(page);
|
||||||
|
|
||||||
// remove user4
|
// remove user4
|
||||||
await page.locator('.select-assignees.dropdown').click();
|
await page.locator('.select-assignees.dropdown').click();
|
||||||
|
@ -220,6 +221,7 @@ test('New Issue: Assignees', async ({browser}, workerInfo) => {
|
||||||
await page.fill('.select-assignees .menu .search input', '');
|
await page.fill('.select-assignees .menu .search input', '');
|
||||||
await page.locator('.select-assignees.dropdown .no-select.item').click();
|
await page.locator('.select-assignees.dropdown .no-select.item').click();
|
||||||
await expect(page.locator('.select-assign-me')).toBeVisible();
|
await expect(page.locator('.select-assign-me')).toBeVisible();
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Issue: Milestone', async ({browser}, workerInfo) => {
|
test('Issue: Milestone', async ({browser}, workerInfo) => {
|
||||||
|
@ -256,14 +258,17 @@ test('New Issue: Milestone', async ({browser}, workerInfo) => {
|
||||||
const selectedMilestone = page.locator('.issue-content-right .select-milestone.list');
|
const selectedMilestone = page.locator('.issue-content-right .select-milestone.list');
|
||||||
const milestoneDropdown = page.locator('.issue-content-right .select-milestone.dropdown');
|
const milestoneDropdown = page.locator('.issue-content-right .select-milestone.dropdown');
|
||||||
await expect(selectedMilestone).toContainText('No milestone');
|
await expect(selectedMilestone).toContainText('No milestone');
|
||||||
|
await save_visual(page);
|
||||||
|
|
||||||
// Add milestone.
|
// Add milestone.
|
||||||
await milestoneDropdown.click();
|
await milestoneDropdown.click();
|
||||||
await page.getByRole('option', {name: 'milestone1'}).click();
|
await page.getByRole('option', {name: 'milestone1'}).click();
|
||||||
await expect(selectedMilestone).toContainText('milestone1');
|
await expect(selectedMilestone).toContainText('milestone1');
|
||||||
|
await save_visual(page);
|
||||||
|
|
||||||
// Clear milestone.
|
// Clear milestone.
|
||||||
await milestoneDropdown.click();
|
await milestoneDropdown.click();
|
||||||
await page.getByText('Clear milestone', {exact: true}).click();
|
await page.getByText('Clear milestone', {exact: true}).click();
|
||||||
await expect(selectedMilestone).toContainText('No milestone');
|
await expect(selectedMilestone).toContainText('No milestone');
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
import {test, load_logged_in_context, login_user} from './utils_e2e.ts';
|
import {test, save_visual, load_logged_in_context, login_user} from './utils_e2e.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
await login_user(browser, workerInfo, 'user2');
|
await login_user(browser, workerInfo, 'user2');
|
||||||
|
@ -40,6 +40,7 @@ test('Markdown image preview behaviour', async ({browser}, workerInfo) => {
|
||||||
// Check for the image preview via the expected attribute
|
// Check for the image preview via the expected attribute
|
||||||
const preview = page.locator('div[data-tab="preview"] p[dir="auto"] a');
|
const preview = page.locator('div[data-tab="preview"] p[dir="auto"] a');
|
||||||
await expect(preview).toHaveAttribute('href', 'http://localhost:3003/user2/repo1/media/branch/master/assets/logo.svg');
|
await expect(preview).toHaveAttribute('href', 'http://localhost:3003/user2/repo1/media/branch/master/assets/logo.svg');
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('markdown indentation', async ({browser}, workerInfo) => {
|
test('markdown indentation', async ({browser}, workerInfo) => {
|
||||||
|
@ -224,6 +225,7 @@ test('markdown insert table', async ({browser}, workerInfo) => {
|
||||||
|
|
||||||
const newTableModal = page.locator('div[data-markdown-table-modal-id="0"]');
|
const newTableModal = page.locator('div[data-markdown-table-modal-id="0"]');
|
||||||
await expect(newTableModal).toBeVisible();
|
await expect(newTableModal).toBeVisible();
|
||||||
|
await save_visual(page);
|
||||||
|
|
||||||
await newTableModal.locator('input[name="table-rows"]').fill('3');
|
await newTableModal.locator('input[name="table-rows"]').fill('3');
|
||||||
await newTableModal.locator('input[name="table-columns"]').fill('2');
|
await newTableModal.locator('input[name="table-columns"]').fill('2');
|
||||||
|
@ -234,4 +236,5 @@ test('markdown insert table', async ({browser}, workerInfo) => {
|
||||||
|
|
||||||
const textarea = page.locator('textarea[name=content]');
|
const textarea = page.locator('textarea[name=content]');
|
||||||
await expect(textarea).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
|
await expect(textarea).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
import {test, login_user, login} from './utils_e2e.ts';
|
import {test, save_visual, login_user, login} from './utils_e2e.ts';
|
||||||
import {validate_form} from './shared/forms.ts';
|
import {validate_form} from './shared/forms.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
|
@ -20,9 +20,11 @@ test('org team settings', async ({browser}, workerInfo) => {
|
||||||
|
|
||||||
await page.locator('input[name="permission"][value="admin"]').click();
|
await page.locator('input[name="permission"][value="admin"]').click();
|
||||||
await expect(page.locator('.hide-unless-checked')).toBeHidden();
|
await expect(page.locator('.hide-unless-checked')).toBeHidden();
|
||||||
|
await save_visual(page);
|
||||||
|
|
||||||
await page.locator('input[name="permission"][value="read"]').click();
|
await page.locator('input[name="permission"][value="read"]').click();
|
||||||
await expect(page.locator('.hide-unless-checked')).toBeVisible();
|
await expect(page.locator('.hide-unless-checked')).toBeVisible();
|
||||||
|
await save_visual(page);
|
||||||
|
|
||||||
// we are validating the form here to include the part that could be hidden
|
// we are validating the form here to include the part that could be hidden
|
||||||
await validate_form({page});
|
await validate_form({page});
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
import {test, login_user, load_logged_in_context} from './utils_e2e.ts';
|
import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts';
|
||||||
|
|
||||||
test('Follow actions', async ({browser}, workerInfo) => {
|
test('Follow actions', async ({browser}, workerInfo) => {
|
||||||
await login_user(browser, workerInfo, 'user2');
|
await login_user(browser, workerInfo, 'user2');
|
||||||
|
@ -29,6 +29,7 @@ test('Follow actions', async ({browser}, workerInfo) => {
|
||||||
|
|
||||||
await page.locator('.block').click();
|
await page.locator('.block').click();
|
||||||
await expect(page.locator('#block-user')).toBeVisible();
|
await expect(page.locator('#block-user')).toBeVisible();
|
||||||
|
await save_visual(page);
|
||||||
await page.locator('#block-user .ok').click();
|
await page.locator('#block-user .ok').click();
|
||||||
await expect(page.locator('.block')).toContainText('Unblock');
|
await expect(page.locator('.block')).toContainText('Unblock');
|
||||||
await expect(page.locator('#block-user')).toBeHidden();
|
await expect(page.locator('#block-user')).toBeHidden();
|
||||||
|
@ -38,6 +39,7 @@ test('Follow actions', async ({browser}, workerInfo) => {
|
||||||
const flashMessage = page.locator('#flash-message');
|
const flashMessage = page.locator('#flash-message');
|
||||||
await expect(flashMessage).toBeVisible();
|
await expect(flashMessage).toBeVisible();
|
||||||
await expect(flashMessage).toContainText('You cannot follow this user because you have blocked this user or this user has blocked you.');
|
await expect(flashMessage).toContainText('You cannot follow this user because you have blocked this user or this user has blocked you.');
|
||||||
|
await save_visual(page);
|
||||||
|
|
||||||
// Unblock interaction.
|
// Unblock interaction.
|
||||||
await page.locator('.block').click();
|
await page.locator('.block').click();
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect, type Locator} from '@playwright/test';
|
import {expect, type Locator} from '@playwright/test';
|
||||||
import {test, login_user, load_logged_in_context} from './utils_e2e.ts';
|
import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
await login_user(browser, workerInfo, 'user2');
|
await login_user(browser, workerInfo, 'user2');
|
||||||
|
@ -66,4 +66,5 @@ test('Reaction Selectors', async ({browser}, workerInfo) => {
|
||||||
|
|
||||||
await toggleReaction(topPicker, 'laugh');
|
await toggleReaction(topPicker, 'laugh');
|
||||||
await assertReactionCounts(comment, {'laugh': 2});
|
await assertReactionCounts(comment, {'laugh': 2});
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
|
@ -41,7 +41,7 @@ test('External Release Attachments', async ({browser, isMobile}, workerInfo) =>
|
||||||
await page.fill('input[name=attachment-new-name-2]', 'Test');
|
await page.fill('input[name=attachment-new-name-2]', 'Test');
|
||||||
await page.fill('input[name=attachment-new-exturl-2]', 'https://forgejo.org/');
|
await page.fill('input[name=attachment-new-exturl-2]', 'https://forgejo.org/');
|
||||||
await page.click('.remove-rel-attach');
|
await page.click('.remove-rel-attach');
|
||||||
save_visual(page);
|
await save_visual(page);
|
||||||
await page.click('.button.small.primary');
|
await page.click('.button.small.primary');
|
||||||
|
|
||||||
// Validate release page and click edit
|
// Validate release page and click edit
|
||||||
|
@ -53,7 +53,7 @@ test('External Release Attachments', async ({browser, isMobile}, workerInfo) =>
|
||||||
await expect(page.locator('.download[open] li:nth-of-type(2) a')).toHaveAttribute('href', '/user2/repo2/archive/2.0.tar.gz');
|
await expect(page.locator('.download[open] li:nth-of-type(2) a')).toHaveAttribute('href', '/user2/repo2/archive/2.0.tar.gz');
|
||||||
await expect(page.locator('.download[open] li:nth-of-type(3)')).toContainText('Test');
|
await expect(page.locator('.download[open] li:nth-of-type(3)')).toContainText('Test');
|
||||||
await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://forgejo.org/');
|
await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://forgejo.org/');
|
||||||
save_visual(page);
|
await save_visual(page);
|
||||||
await page.locator('.octicon-pencil').first().click();
|
await page.locator('.octicon-pencil').first().click();
|
||||||
|
|
||||||
// Validate edit page and edit the release
|
// Validate edit page and edit the release
|
||||||
|
@ -68,7 +68,7 @@ test('External Release Attachments', async ({browser, isMobile}, workerInfo) =>
|
||||||
await expect(page.locator('.attachment_edit:visible')).toHaveCount(4);
|
await expect(page.locator('.attachment_edit:visible')).toHaveCount(4);
|
||||||
await page.locator('.attachment_edit:visible').nth(2).fill('Test3');
|
await page.locator('.attachment_edit:visible').nth(2).fill('Test3');
|
||||||
await page.locator('.attachment_edit:visible').nth(3).fill('https://gitea.com/');
|
await page.locator('.attachment_edit:visible').nth(3).fill('https://gitea.com/');
|
||||||
save_visual(page);
|
await save_visual(page);
|
||||||
await page.click('.button.small.primary');
|
await page.click('.button.small.primary');
|
||||||
|
|
||||||
// Validate release page and click edit
|
// Validate release page and click edit
|
||||||
|
@ -78,7 +78,7 @@ test('External Release Attachments', async ({browser, isMobile}, workerInfo) =>
|
||||||
await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://gitea.io/');
|
await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://gitea.io/');
|
||||||
await expect(page.locator('.download[open] li:nth-of-type(4)')).toContainText('Test3');
|
await expect(page.locator('.download[open] li:nth-of-type(4)')).toContainText('Test3');
|
||||||
await expect(page.locator('.download[open] li:nth-of-type(4) a')).toHaveAttribute('href', 'https://gitea.com/');
|
await expect(page.locator('.download[open] li:nth-of-type(4) a')).toHaveAttribute('href', 'https://gitea.com/');
|
||||||
save_visual(page);
|
await save_visual(page);
|
||||||
await page.locator('.octicon-pencil').first().click();
|
await page.locator('.octicon-pencil').first().click();
|
||||||
|
|
||||||
// Delete release
|
// Delete release
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect, type Page} from '@playwright/test';
|
import {expect, type Page} from '@playwright/test';
|
||||||
import {test, login_user, login} from './utils_e2e.ts';
|
import {test, save_visual, login_user, login} from './utils_e2e.ts';
|
||||||
import {accessibilityCheck} from './shared/accessibility.ts';
|
import {accessibilityCheck} from './shared/accessibility.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
|
@ -89,10 +89,12 @@ test('Username highlighted in commits', async ({browser}, workerInfo) => {
|
||||||
await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/);
|
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 expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||||
await accessibilityCheck({page}, ['.commit-header'], [], []);
|
await accessibilityCheck({page}, ['.commit-header'], [], []);
|
||||||
|
await save_visual(page);
|
||||||
// check second commit
|
// check second commit
|
||||||
await page.goto('/user2/mentions-highlighted/commits/branch/main');
|
await page.goto('/user2/mentions-highlighted/commits/branch/main');
|
||||||
await page.locator('tbody').getByRole('link', {name: 'Another commit which mentions'}).click();
|
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: '@user2'})).toHaveCSS('background-color', /(.*)/);
|
||||||
await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||||
await accessibilityCheck({page}, ['.commit-header'], [], []);
|
await accessibilityCheck({page}, ['.commit-header'], [], []);
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
import {test, login_user, load_logged_in_context} from './utils_e2e.ts';
|
import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts';
|
||||||
|
|
||||||
test.beforeAll(({browser}, workerInfo) => login_user(browser, workerInfo, 'user2'));
|
test.beforeAll(({browser}, workerInfo) => login_user(browser, workerInfo, 'user2'));
|
||||||
|
|
||||||
|
@ -19,17 +19,22 @@ test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo
|
||||||
const form = page.locator('form');
|
const form = page.locator('form');
|
||||||
await form.getByRole('textbox', {name: 'Repository Name'}).fill('invalidrepo');
|
await form.getByRole('textbox', {name: 'Repository Name'}).fill('invalidrepo');
|
||||||
await form.getByRole('textbox', {name: 'Migrate / Clone from URL'}).fill('https://codeberg.org/forgejo/invalidrepo');
|
await form.getByRole('textbox', {name: 'Migrate / Clone from URL'}).fill('https://codeberg.org/forgejo/invalidrepo');
|
||||||
|
await save_visual(page);
|
||||||
await form.locator('button.primary').click({timeout: 5000});
|
await form.locator('button.primary').click({timeout: 5000});
|
||||||
await expect(page).toHaveURL('user2/invalidrepo');
|
await expect(page).toHaveURL('user2/invalidrepo');
|
||||||
|
await save_visual(page);
|
||||||
|
// page screenshot of unauthedPage is checked automatically after the test
|
||||||
|
|
||||||
expect((await unauthedPage.goto('/user2/invalidrepo'))?.status(), 'public migration page should be accessible').toBe(200);
|
expect((await unauthedPage.goto('/user2/invalidrepo'))?.status(), 'public migration page should be accessible').toBe(200);
|
||||||
await expect(unauthedPage.locator('#repo_migrating_progress')).toBeVisible();
|
await expect(unauthedPage.locator('#repo_migrating_progress')).toBeVisible();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.locator('#repo_migrating_failed')).toBeVisible();
|
await expect(page.locator('#repo_migrating_failed')).toBeVisible();
|
||||||
|
await save_visual(page);
|
||||||
await page.getByRole('button', {name: 'Delete this repository'}).click();
|
await page.getByRole('button', {name: 'Delete this repository'}).click();
|
||||||
const deleteModal = page.locator('#delete-repo-modal');
|
const deleteModal = page.locator('#delete-repo-modal');
|
||||||
await deleteModal.getByRole('textbox', {name: 'Confirmation string'}).fill('user2/invalidrepo');
|
await deleteModal.getByRole('textbox', {name: 'Confirmation string'}).fill('user2/invalidrepo');
|
||||||
|
await save_visual(page);
|
||||||
await deleteModal.getByRole('button', {name: 'Delete repository'}).click();
|
await deleteModal.getByRole('button', {name: 'Delete repository'}).click();
|
||||||
await expect(page).toHaveURL('/');
|
await expect(page).toHaveURL('/');
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
import {test, login_user, login} from './utils_e2e.ts';
|
import {test, save_visual, login_user, login} from './utils_e2e.ts';
|
||||||
import {validate_form} from './shared/forms.ts';
|
import {validate_form} from './shared/forms.ts';
|
||||||
|
|
||||||
test.beforeAll(async ({browser}, workerInfo) => {
|
test.beforeAll(async ({browser}, workerInfo) => {
|
||||||
|
@ -25,11 +25,13 @@ test('repo webhook settings', async ({browser}, workerInfo) => {
|
||||||
|
|
||||||
// check accessibility including the custom events (now visible) part
|
// check accessibility including the custom events (now visible) part
|
||||||
await validate_form({page}, 'fieldset');
|
await validate_form({page}, 'fieldset');
|
||||||
|
await save_visual(page);
|
||||||
|
|
||||||
await page.locator('input[name="events"][value="push_only"]').click();
|
await page.locator('input[name="events"][value="push_only"]').click();
|
||||||
await expect(page.locator('.hide-unless-checked')).toBeHidden();
|
await expect(page.locator('.hide-unless-checked')).toBeHidden();
|
||||||
await page.locator('input[name="events"][value="send_everything"]').click();
|
await page.locator('input[name="events"][value="send_everything"]').click();
|
||||||
await expect(page.locator('.hide-unless-checked')).toBeHidden();
|
await expect(page.locator('.hide-unless-checked')).toBeHidden();
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('repo branch protection settings', () => {
|
test.describe('repo branch protection settings', () => {
|
||||||
|
@ -44,11 +46,14 @@ test.describe('repo branch protection settings', () => {
|
||||||
// verify header is new
|
// verify header is new
|
||||||
await expect(page.locator('h4')).toContainText('new');
|
await expect(page.locator('h4')).toContainText('new');
|
||||||
await page.locator('input[name="rule_name"]').fill('testrule');
|
await page.locator('input[name="rule_name"]').fill('testrule');
|
||||||
|
await save_visual(page);
|
||||||
await page.getByText('Save rule').click();
|
await page.getByText('Save rule').click();
|
||||||
// verify header is in edit mode
|
// verify header is in edit mode
|
||||||
await page.waitForLoadState('domcontentloaded');
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await save_visual(page);
|
||||||
await page.getByText('Edit').click();
|
await page.getByText('Edit').click();
|
||||||
await expect(page.locator('h4')).toContainText('Protection rules for branch');
|
await expect(page.locator('h4')).toContainText('Protection rules for branch');
|
||||||
|
await save_visual(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach(async ({browser}, workerInfo) => {
|
test.afterEach(async ({browser}, workerInfo) => {
|
||||||
|
|
|
@ -4,6 +4,15 @@ export const test = baseTest.extend({
|
||||||
context: async ({browser}, use) => {
|
context: async ({browser}, use) => {
|
||||||
return use(await test_context(browser));
|
return use(await test_context(browser));
|
||||||
},
|
},
|
||||||
|
// see https://playwright.dev/docs/test-fixtures#adding-global-beforeeachaftereach-hooks
|
||||||
|
forEachTest: [async ({page}, use) => {
|
||||||
|
await use();
|
||||||
|
// some tests create a new page which is not yet available here
|
||||||
|
// only operate on tests that make the URL available
|
||||||
|
if (page.url() !== 'about:blank') {
|
||||||
|
await save_visual(page);
|
||||||
|
}
|
||||||
|
}, {auto: true}],
|
||||||
});
|
});
|
||||||
|
|
||||||
async function test_context(browser: Browser, options?: BrowserContextOptions) {
|
async function test_context(browser: Browser, options?: BrowserContextOptions) {
|
||||||
|
@ -66,14 +75,28 @@ export async function save_visual(page: Page) {
|
||||||
// Optionally include visual testing
|
// Optionally include visual testing
|
||||||
if (process.env.VISUAL_TEST) {
|
if (process.env.VISUAL_TEST) {
|
||||||
await page.waitForLoadState('domcontentloaded');
|
await page.waitForLoadState('domcontentloaded');
|
||||||
// Mock page/version string
|
// Mock/replace dynamic content which can have different size (and thus cannot simply be masked below)
|
||||||
await page.locator('footer div.ui.left').evaluate((node) => node.innerHTML = 'MOCK');
|
await page.locator('footer .left-links').evaluate((node) => node.innerHTML = 'MOCK');
|
||||||
|
// replace timestamps in repos to mask them later down
|
||||||
|
await page.locator('.flex-item-body > relative-time').filter({hasText: /now|minute/}).evaluateAll((nodes) => {
|
||||||
|
for (const node of nodes) node.outerHTML = 'relative time in repo';
|
||||||
|
});
|
||||||
|
await page.locator('relative-time').evaluateAll((nodes) => {
|
||||||
|
for (const node of nodes) node.outerHTML = 'time element';
|
||||||
|
});
|
||||||
|
// used for instance for security keys
|
||||||
|
await page.locator('absolute-date').evaluateAll((nodes) => {
|
||||||
|
for (const node of nodes) node.outerHTML = 'time element';
|
||||||
|
});
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
fullPage: true,
|
fullPage: true,
|
||||||
timeout: 20000,
|
timeout: 20000,
|
||||||
mask: [
|
mask: [
|
||||||
page.locator('.secondary-nav span>img.ui.avatar'),
|
page.locator('.ui.avatar'),
|
||||||
page.locator('.ui.dropdown.jump.item span>img.ui.avatar'),
|
page.locator('.sha'),
|
||||||
|
page.locator('#repo_migrating'),
|
||||||
|
// update order of recently created repos is not fully deterministic
|
||||||
|
page.locator('.flex-item-main').filter({hasText: 'relative time in repo'}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
// @watch end
|
// @watch end
|
||||||
|
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
import {test, create_temp_user, login_user} from './utils_e2e.ts';
|
import {test, save_visual, create_temp_user, login_user} from './utils_e2e.ts';
|
||||||
|
|
||||||
test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => {
|
test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => {
|
||||||
test.skip(workerInfo.project.name !== 'chromium', 'Uses Chrome protocol');
|
test.skip(workerInfo.project.name !== 'chromium', 'Uses Chrome protocol');
|
||||||
|
@ -34,6 +34,7 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.locator('input#nickname').fill('Testing Security Key');
|
await page.locator('input#nickname').fill('Testing Security Key');
|
||||||
|
await save_visual(page);
|
||||||
await page.getByText('Add security key').click();
|
await page.getByText('Add security key').click();
|
||||||
|
|
||||||
// Logout.
|
// Logout.
|
||||||
|
@ -57,6 +58,7 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) =>
|
||||||
response = await page.goto('/user/settings/security');
|
response = await page.goto('/user/settings/security');
|
||||||
expect(response?.status()).toBe(200);
|
expect(response?.status()).toBe(200);
|
||||||
await page.getByRole('button', {name: 'Remove'}).click();
|
await page.getByRole('button', {name: 'Remove'}).click();
|
||||||
|
await save_visual(page);
|
||||||
await page.getByRole('button', {name: 'Yes'}).click();
|
await page.getByRole('button', {name: 'Yes'}).click();
|
||||||
await page.waitForLoadState();
|
await page.waitForLoadState();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue