e2e: Selective screenshots (#9499)

* Add some test that only snapshot relevant content
* Allow adding marging around the element in case the environment is relevant (e.g. the location of an element relative to the parent, but excluding the environment of the parent)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9499
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Otto Richter <git@otto.splvs.net>
Co-committed-by: Otto Richter <git@otto.splvs.net>
This commit is contained in:
Otto Richter 2025-10-03 13:45:08 +02:00 committed by Otto
commit f87b76160e
34 changed files with 264 additions and 186 deletions

View file

@ -10,7 +10,8 @@
// @watch end
import {expect, type Page, type TestInfo} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
const workflow_trigger_notification_text = 'This workflow has a workflow_dispatch event trigger.';
@ -21,13 +22,13 @@ async function dispatchSuccess(page: Page, testInfo: TestInfo) {
await page.locator('#workflow_dispatch_dropdown>button').click();
await page.fill('input[name="inputs[string2]"]', 'abc');
await save_visual(page);
await screenshot(page, page.locator('div.ui.container').filter({hasText: 'All workflows'}));
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);
await screenshot(page, page.locator('div.ui.container').filter({hasText: 'All workflows'}));
}
test.describe('Workflow Authenticated user2', () => {
@ -45,7 +46,7 @@ test.describe('Workflow Authenticated user2', () => {
await expect(menu).toBeHidden();
await run_workflow_btn.click();
await expect(menu).toBeVisible();
await save_visual(page);
await screenshot(page, page.locator('div.ui.container').filter({hasText: 'All workflows'}));
});
test('dispatch error: missing inputs', async ({page}, testInfo) => {
@ -64,7 +65,7 @@ test.describe('Workflow Authenticated user2', () => {
await page.locator('#workflow-dispatch-submit').click();
await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible();
await save_visual(page);
await screenshot(page, page.locator('div.ui.container').filter({hasText: 'All workflows'}));
});
// no assertions as the login in this test case is extracted for reuse
@ -78,7 +79,7 @@ test('workflow dispatch box not available for unauthenticated users', async ({pa
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
await expect(page.locator('body')).not.toContainText(workflow_trigger_notification_text);
await save_visual(page);
await screenshot(page, page.locator('div.ui.container').filter({hasText: 'All workflows'}));
});
async function completeDynamicRefresh(page: Page) {
@ -119,7 +120,7 @@ test.describe('workflow list dynamic refresh', () => {
});
await completeDynamicRefresh(page);
await expect(backgroundPage.locator('.run-list>:first-child .flex-item-body>b', {hasText: latestDispatchedRun})).toBeVisible();
await save_visual(backgroundPage);
await screenshot(backgroundPage, page.locator('div.ui.container').filter({hasText: 'All workflows'}));
});
test('refreshes on interval', async ({page}, testInfo) => {
@ -137,7 +138,7 @@ test.describe('workflow list dynamic refresh', () => {
await simulatePollingInterval(backgroundPage);
await expect(backgroundPage.locator('.run-list>:first-child .flex-item-body>b', {hasText: latestDispatchedRun})).toBeVisible();
await save_visual(backgroundPage);
await screenshot(backgroundPage, page.locator('div.ui.container').filter({hasText: 'All workflows'}));
});
test('post-refresh the dropdowns continue to operate', async ({page}, testInfo) => {

View file

@ -8,7 +8,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test('copy src file path to clipboard', async ({page}, workerInfo) => {
test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'Apple clipboard API addon - starting at just $499!');
@ -20,7 +21,7 @@ test('copy src file path to clipboard', async ({page}, workerInfo) => {
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
await expect(page.getByText('Copied')).toBeVisible();
await save_visual(page);
await screenshot(page, page.getByText('Copied'), 50);
});
test('copy diff file path to clipboard', async ({page}, workerInfo) => {
@ -33,5 +34,5 @@ test('copy diff file path to clipboard', async ({page}, workerInfo) => {
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
await expect(page.getByText('Copied')).toBeVisible();
await save_visual(page);
await screenshot(page, page.getByText('Copied'), 50);
});

View file

@ -24,5 +24,5 @@ test('Correct link and tooltip', async ({page}, testInfo) => {
await expect(repoStatus).toHaveAttribute('href', '/user2/test_workflows/actions', {timeout: 10000});
await expect(repoStatus).toHaveAttribute('data-tooltip-content', /^(Error|Failure)$/);
// ToDo: Ensure stable screenshot of dashboard. Known to be flaky: https://code.forgejo.org/forgejo/visual-browser-testing/commit/206d4cfb7a4af6d8d7043026cdd4d63708798b2a
// await save_visual(page);
// await screenshot(page);
});

View file

@ -5,7 +5,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -23,13 +24,13 @@ test('Dimmed modal', async ({page}) => {
// Modal and dimmer should be visible.
await expect(page.locator('#block-user')).toBeVisible();
await expect(page.locator('.ui.dimmer')).toBeVisible();
await save_visual(page);
await screenshot(page, page.locator('.ui.g-modal-confirm.delete.modal'), 50);
// After canceling, modal and dimmer should be hidden.
await page.locator('#block-user .cancel').click();
await expect(page.locator('.ui.dimmer')).toBeHidden();
await expect(page.locator('#block-user')).toBeHidden();
await save_visual(page);
await screenshot(page);
// Open the block modal and make the dimmer visible again.
await page.locator('.actions .dropdown').click();
@ -37,7 +38,7 @@ test('Dimmed modal', async ({page}) => {
await expect(page.locator('#block-user')).toBeVisible();
await expect(page.locator('.ui.dimmer')).toBeVisible();
await expect(page.locator('.ui.dimmer')).toHaveCount(1);
await save_visual(page);
await screenshot(page, page.locator('.ui.g-modal-confirm.delete.modal'), 50);
});
test('Dimmed overflow', async ({page}, workerInfo) => {
@ -58,7 +59,7 @@ test('Dimmed overflow', async ({page}, workerInfo) => {
// Expect a 'are you sure, this file is empty' modal.
await expect(page.locator('#edit-empty-content-modal')).toBeVisible();
await expect(page.locator('#edit-empty-content-modal header')).toContainText('Commit an empty file');
await save_visual(page);
await screenshot(page);
// Trickery to check the page cannot be scrolled.
const {overflow} = await page.evaluate(() => {

View file

@ -5,7 +5,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test('Load Homepage', async ({page}) => {
const response = await page.goto('/');
@ -26,7 +27,7 @@ test('Register Form', async ({page}, workerInfo) => {
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
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 save_visual(page);
await screenshot(page);
});
// eslint-disable-next-line playwright/no-skipped-test

View file

@ -7,7 +7,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test('Explore view taborder', async ({page}) => {
await page.goto('/explore/repos');
@ -42,5 +43,5 @@ test('Explore view taborder', async ({page}) => {
}
}
expect(res).toBe(exp);
await save_visual(page);
await screenshot(page);
});

View file

@ -1,6 +1,7 @@
// @ts-check
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -16,15 +17,15 @@ test('Change git note', async ({page}) => {
let textarea = page.locator('textarea[name="notes"]');
await expect(textarea).toBeVisible();
await textarea.fill('This is a new note');
await save_visual(page);
await screenshot(page, page.locator('.ui.container.fluid.padded'));
await page.locator('#notes-save-button').click();
await save_visual(page);
await screenshot(page, page.locator('.ui.container.fluid.padded'));
response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
expect(response?.status()).toBe(200);
textarea = page.locator('textarea[name="notes"]');
await expect(textarea).toHaveText('This is a new note');
await save_visual(page);
await screenshot(page, page.locator('.ui.container.fluid.padded'));
});

View file

@ -6,7 +6,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test, dynamic_id} from './utils_e2e.ts';
import {test, dynamic_id} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -44,7 +45,7 @@ test('Repository image diff', async ({page}) => {
await expect(page.locator('.tab[data-tab="diff-side-by-side-1"]')).toBeVisible();
await expect(page.locator('.tab[data-tab="diff-swipe-1"]')).toBeHidden();
await expect(page.locator('.tab[data-tab="diff-overlay-1"]')).toBeHidden();
await save_visual(page);
await screenshot(page, page.locator('#diff-container'));
await page.getByText('Swipe').click();
await expect(page.locator('.item[data-tab="diff-side-by-side-1"]')).not.toContainClass('active');
@ -53,7 +54,7 @@ test('Repository image diff', async ({page}) => {
await expect(page.locator('.tab[data-tab="diff-side-by-side-1"]')).toBeHidden();
await expect(page.locator('.tab[data-tab="diff-swipe-1"]')).toBeVisible();
await expect(page.locator('.tab[data-tab="diff-overlay-1"]')).toBeHidden();
await save_visual(page);
await screenshot(page, page.locator('#diff-container'));
await page.getByText('Overlay').click();
await expect(page.locator('.item[data-tab="diff-side-by-side-1"]')).not.toContainClass('active');
@ -62,5 +63,5 @@ test('Repository image diff', async ({page}) => {
await expect(page.locator('.tab[data-tab="diff-side-by-side-1"]')).toBeHidden();
await expect(page.locator('.tab[data-tab="diff-swipe-1"]')).toBeHidden();
await expect(page.locator('.tab[data-tab="diff-overlay-1"]')).toBeVisible();
await save_visual(page);
await screenshot(page, page.locator('#diff-container'));
});

View file

@ -9,7 +9,8 @@
// @watch end
import {expect, type Locator, type Page, type TestInfo} from '@playwright/test';
import {test, save_visual, dynamic_id} from './utils_e2e.ts';
import {test, dynamic_id} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -67,7 +68,7 @@ test('Paste image in new comment', async ({page}, workerInfo) => {
await expect(preview.locator('.octicon-copy')).toBeVisible();
await assertCopy(page, workerInfo, '![foo](');
await save_visual(page);
await screenshot(page, page.locator('.issue-content-left'));
});
test('Re-add images to dropzone on edit', async ({page}, workerInfo) => {
@ -90,5 +91,5 @@ test('Re-add images to dropzone on edit', async ({page}, workerInfo) => {
await expect(preview.locator('.octicon-copy')).toBeVisible();
await assertCopy(page, workerInfo, '![foo](');
await save_visual(page);
await screenshot(page, page.locator('.issue-content-left'));
});

View file

@ -6,7 +6,8 @@
// @watch end
import {expect} from '@playwright/test';
import {test, dynamic_id, save_visual} from './utils_e2e.ts';
import {test, dynamic_id} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -75,7 +76,7 @@ test('Always focus edit tab first on edit', async ({page}) => {
await expect(editTab).toHaveClass(/active/);
await expect(previewTab).not.toHaveClass(/active/);
await save_visual(page);
await screenshot(page, page.locator('.issue-content-left'));
});
test('Reset content of comment edit field on cancel', async ({page}) => {
@ -96,7 +97,7 @@ test('Reset content of comment edit field on cancel', async ({page}) => {
await page.click('#issue-1 .comment-container .context-menu');
await page.click('#issue-1 .comment-container .menu>.edit-content');
await expect(editorTextarea).toHaveValue('content for the first issue');
await save_visual(page);
await screenshot(page, page.locator('.issue-content-left'));
});
test('Quote reply', async ({page}, workerInfo) => {

View file

@ -7,7 +7,8 @@
/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["check_wip"] }] */
import {expect, type Page} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -194,7 +195,7 @@ test('New Issue: Assignees', async ({page}, workerInfo) => {
await page.locator('.select-assignees .menu .item').filter({hasText: 'user4'}).click();
await page.locator('.select-assignees.dropdown').click();
await expect(assigneesList.filter({hasText: 'user4'})).toBeVisible();
await save_visual(page);
await screenshot(page, page.locator('.issue-content-right'));
// remove user4
await page.locator('.select-assignees.dropdown').click();
@ -212,7 +213,7 @@ test('New Issue: Assignees', async ({page}, workerInfo) => {
await page.fill('.select-assignees .menu .search input', '');
await page.locator('.select-assignees.dropdown .no-select.item').click();
await expect(page.locator('.select-assign-me')).toBeVisible();
await save_visual(page);
await screenshot(page, page.locator('div.filter.menu[data-id="#assignee_ids"]'), 30);
});
test('Issue: Milestone', async ({page}, workerInfo) => {
@ -247,19 +248,20 @@ test('New Issue: Milestone', async ({page}, workerInfo) => {
const selectedMilestone = page.locator('.issue-content-right .select-milestone.list');
const milestoneDropdown = page.locator('.issue-content-right .select-milestone.dropdown');
await expect(selectedMilestone).toContainText('No milestone');
await save_visual(page);
await screenshot(page, page.locator('.issue-content-right'));
// Add milestone.
await milestoneDropdown.click();
await screenshot(page, page.locator('.menu.transition.visible'), 30);
await page.getByRole('option', {name: 'milestone1'}).click();
await expect(selectedMilestone).toContainText('milestone1');
await save_visual(page);
await screenshot(page, page.locator('.issue-content-right'));
// Clear milestone.
await milestoneDropdown.click();
await page.getByText('Clear milestone', {exact: true}).click();
await expect(selectedMilestone).toContainText('No milestone');
await save_visual(page);
await screenshot(page, page.locator('.issue-content-right'));
});
test.describe('Dependency dropdown', () => {

View file

@ -5,7 +5,8 @@
// @watch end
import {expect} from '@playwright/test';
import {test, save_visual} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -31,7 +32,7 @@ test('Issue timetracking', async ({page}) => {
// Verify it is shown in the issue sidebar
await expect(page.locator('.issue-content-right .comments')).toContainText('Total time spent: 5 hours 32 minutes');
await save_visual(page);
await screenshot(page);
// Delete the added time.
await page.getByRole('button', {name: 'Delete this time log'}).click();

View file

@ -8,7 +8,8 @@
// @watch end
import {expect} from '@playwright/test';
import {test, save_visual, test_context} from './utils_e2e.ts';
import {test, test_context} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test('Mismatched ROOT_URL', async ({browser}, workerInfo) => {
test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'init script gets randomly ignored');
@ -27,7 +28,7 @@ test('Mismatched ROOT_URL', async ({browser}, workerInfo) => {
const response = await page.goto('/user/login');
expect(response?.status()).toBe(200);
await save_visual(page);
await screenshot(page);
const globalError = page.locator('.js-global-error');
await expect(globalError).toContainText('This Forgejo instance is configured to be served on ');
await expect(globalError).toContainText('You are currently viewing Forgejo through a different URL, which may cause parts of the application to break. The canonical URL is controlled by Forgejo admins via the ROOT_URL setting in the app.ini.');

View file

@ -8,7 +8,8 @@
import {expect} from '@playwright/test';
import {accessibilityCheck} from './shared/accessibility.ts';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -38,7 +39,7 @@ test('Markdown image preview behaviour', async ({page}, workerInfo) => {
// Check for the image preview via the expected attribute
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 save_visual(page);
await screenshot(page);
});
test('Markdown indentation via toolbar', async ({page}) => {
@ -359,7 +360,7 @@ test('Markdown insert table', async ({page}) => {
const newTableModal = page.locator('div[data-markdown-table-modal-id="0"]');
await expect(newTableModal).toBeVisible();
await save_visual(page);
await screenshot(page);
await newTableModal.locator('input[name="table-rows"]').fill('3');
await newTableModal.locator('input[name="table-columns"]').fill('2');
@ -370,7 +371,7 @@ test('Markdown insert table', async ({page}) => {
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 save_visual(page);
await screenshot(page);
});
test('Markdown insert link', async ({page}) => {
@ -383,7 +384,7 @@ test('Markdown insert link', async ({page}) => {
const newLinkModal = page.locator('div[data-markdown-link-modal-id="0"]');
await expect(newLinkModal).toBeVisible();
await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"]'], [], []);
await save_visual(page);
await screenshot(page);
const url = 'https://example.com';
const description = 'Where does this lead?';
@ -397,7 +398,7 @@ test('Markdown insert link', async ({page}) => {
const textarea = page.locator('textarea[name=content]');
await expect(textarea).toHaveValue(`[${description}](${url})`);
await save_visual(page);
await screenshot(page);
});
test('text expander has higher prio then prefix continuation', async ({page}) => {
@ -451,7 +452,7 @@ test('Combo Markdown: preview mode switch', async ({page}) => {
await expect(toolbarItem).toBeHidden();
await expect(editorPanel).toBeHidden();
await expect(previewPanel).toBeVisible();
await save_visual(page);
await screenshot(page);
// Verify that some content rendered
await expect(page.locator('[data-tab-panel="markdown-previewer"] .emoji[data-alias="100"]')).toBeVisible();
@ -463,7 +464,7 @@ test('Combo Markdown: preview mode switch', async ({page}) => {
await expect(toolbarItem).toBeVisible();
await expect(editorPanel).toBeVisible();
await expect(previewPanel).toBeHidden();
await save_visual(page);
await screenshot(page);
});
test('Multiple combo markdown: insert table', async ({page}) => {
@ -500,7 +501,7 @@ test('Multiple combo markdown: insert table', async ({page}) => {
await expect(comboboxOne).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
await expect(comboboxTwo).toBeEmpty();
await save_visual(page);
await screenshot(page);
// focus second one and add table to it
await textareaTwo.click();
@ -522,7 +523,7 @@ test('Multiple combo markdown: insert table', async ({page}) => {
await expect(comboboxOne).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
await expect(comboboxTwo).toHaveValue('| Header | Header | Header |\n|---------|---------|---------|\n| Content | Content | Content |\n| Content | Content | Content |\n');
await save_visual(page);
await screenshot(page);
});
test('Markdown bold/italic toolbar and shortcut', async ({page}) => {

View file

@ -3,7 +3,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test('markup with #xyz-mode-only', async ({page}, workerInfo) => {
test.skip(['webkit', 'Mobile Safari'].includes(workerInfo.project.name), 'Newest version contains a regression');
@ -14,5 +15,5 @@ test('markup with #xyz-mode-only', async ({page}, workerInfo) => {
await expect(comment).toBeVisible();
await expect(comment.locator('[src$="#gh-light-mode-only"]')).toBeVisible();
await expect(comment.locator('[src$="#gh-dark-mode-only"]')).toBeHidden();
await save_visual(page);
await screenshot(page);
});

View file

@ -7,7 +7,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, dynamic_id, test} from './utils_e2e.ts';
import {dynamic_id, test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -33,7 +34,7 @@ test('Dialog modal', async ({page}, workerInfo) => {
await page.keyboard.press('Backspace');
await page.locator('#commit-button').click();
await save_visual(page);
await screenshot(page);
await expect(page.locator('#edit-empty-content-modal')).toBeVisible();
await page.locator('#edit-empty-content-modal .cancel').click();

View file

@ -5,7 +5,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
import {validate_form} from './shared/forms.ts';
test.use({user: 'user2'});
@ -17,11 +18,11 @@ test('org team settings', async ({page}, workerInfo) => {
await page.locator('input[name="permission"][value="admin"]').click();
await expect(page.locator('.hide-unless-checked')).toBeHidden();
await save_visual(page);
await screenshot(page);
await page.locator('input[name="permission"][value="read"]').click();
await expect(page.locator('.hide-unless-checked')).toBeVisible();
await save_visual(page);
await screenshot(page);
// we are validating the form here to include the part that could be hidden
await validate_form({page});

View file

@ -7,7 +7,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -16,18 +17,18 @@ test('PR: Create review from files', async ({page}) => {
expect(response?.status()).toBe(200);
await expect(page.locator('.tippy-box .review-box-panel')).toBeHidden();
await save_visual(page);
await screenshot(page);
// Review panel should appear after clicking Finish review
await page.locator('#review-box .js-btn-review').click();
await expect(page.locator('.tippy-box .review-box-panel')).toBeVisible();
await save_visual(page);
await screenshot(page);
await page.locator('.review-box-panel textarea#_combo_markdown_editor_0')
.fill('This is a review');
await page.locator('.review-box-panel button.btn-submit[value="approve"]').click();
await page.waitForURL(/.*\/user2\/repo1\/pulls\/5#issuecomment-\d+/);
await save_visual(page);
await screenshot(page);
});
test('PR: Create review from commit', async ({page}) => {
@ -39,7 +40,7 @@ test('PR: Create review from commit', async ({page}) => {
await expect(code_comment).toBeVisible();
await code_comment.fill('This is a code comment');
await save_visual(page);
await screenshot(page);
const start_button = page.locator('.comment-code-cloud form button.btn-start-review');
// Workaround for #7152, where there might already be a pending review state from previous
@ -58,13 +59,13 @@ test('PR: Create review from commit', async ({page}) => {
await page.locator('#review-box .js-btn-review').click();
await expect(page.locator('.tippy-box .review-box-panel')).toBeVisible();
await save_visual(page);
await screenshot(page);
await page.locator('.review-box-panel textarea.markdown-text-editor')
.fill('This is a review');
await page.locator('.review-box-panel button.btn-submit[value="approve"]').click();
await page.waitForURL(/.*\/user2\/repo1\/pulls\/3#issuecomment-\d+/);
await save_visual(page);
await screenshot(page);
// In addition to testing the ability to delete comments, this also
// performs clean up. If tests are run for multiple platforms, the data isn't reset
@ -79,7 +80,7 @@ test('PR: Create review from commit', async ({page}) => {
await page.locator('.comment-header-right.actions div.menu .delete-comment').click();
await expect(page.locator('.comment-list .comment-container')).toBeHidden();
await save_visual(page);
await screenshot(page);
});
test('PR: Navigate by single commit', async ({page}) => {
@ -88,7 +89,7 @@ test('PR: Navigate by single commit', async ({page}) => {
await page.locator('tbody.commit-list td.message a').nth(1).click();
await page.waitForURL(/.*\/user2\/repo1\/pulls\/3\/commits\/4a357436d925b5c974181ff12a994538ddc5a269/);
await save_visual(page);
await screenshot(page);
let prevButton = page.locator('.commit-header-buttons').getByText(/Prev/);
let nextButton = page.locator('.commit-header-buttons').getByText(/Next/);
@ -101,7 +102,7 @@ test('PR: Navigate by single commit', async ({page}) => {
await nextButton.click();
await page.waitForURL(/.*\/user2\/repo1\/pulls\/3\/commits\/5f22f7d0d95d614d25a5b68592adb345a4b5c7fd/);
await save_visual(page);
await screenshot(page);
prevButton = page.locator('.commit-header-buttons').getByText(/Prev/);
nextButton = page.locator('.commit-header-buttons').getByText(/Next/);
@ -122,7 +123,7 @@ test('PR: Test mentions values', async ({page}) => {
await page.locator('.review-box-panel textarea#_combo_markdown_editor_0')
.fill('@');
await save_visual(page);
await screenshot(page);
await expect(page.locator('ul.suggestions li span:first-of-type')).toContainText([
'user1',

View file

@ -7,7 +7,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -16,10 +17,10 @@ test('PR: title edit', async ({page}) => {
expect(response?.status()).toBe(200);
await expect(page.locator('#editable-label')).toBeVisible();
await save_visual(page);
await screenshot(page);
// Labels AGit and Editable are hidden when title is in edit mode
await page.locator('#issue-title-edit-show').click();
await expect(page.locator('#editable-label')).toBeHidden();
await save_visual(page);
await screenshot(page);
});

View file

@ -6,7 +6,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -32,7 +33,7 @@ test('Follow and block actions', async ({page}) => {
await blockButton.click();
await expect(page.locator('#block-user')).toBeVisible();
await save_visual(page);
await screenshot(page);
await page.locator('#block-user .ok').click();
await expect(blockButton).toContainText('Unblock');
await expect(page.locator('#block-user')).toBeHidden();
@ -42,7 +43,7 @@ test('Follow and block actions', async ({page}) => {
const flashMessage = page.locator('#flash-message');
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 save_visual(page);
await screenshot(page);
// Unblock interaction.
await actionsDropdownBtn.click();

View file

@ -4,7 +4,8 @@
// @watch end
import {expect, type Locator} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -62,5 +63,5 @@ test('Reaction Selectors', async ({page}) => {
await toggleReaction(topPicker, 'laugh');
await assertReactionCounts(comment, {'laugh': 2});
await save_visual(page);
await screenshot(page);
});

View file

@ -9,12 +9,13 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
import {validate_form} from './shared/forms.ts';
test.use({user: 'user2'});
test.describe('repo branch protection settings', () => {
test.describe('Releases', () => {
test('External Release Attachments', async ({page, isMobile}, workerInfo) => {
test.skip(isMobile || workerInfo.project.name === 'webkit');
@ -33,7 +34,7 @@ test.describe('repo branch protection settings', () => {
await page.fill('input[name=attachment-new-name-2]', 'Test');
await page.fill('input[name=attachment-new-exturl-2]', 'https://forgejo.org/');
await page.click('.remove-rel-attach');
await save_visual(page);
await screenshot(page);
await page.click('.button.small.primary');
// Validate release page and click edit
@ -52,7 +53,7 @@ test.describe('repo branch protection settings', () => {
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 save_visual(page);
await screenshot(page);
await page.locator('.octicon-pencil').first().click();
// Validate edit page and edit the release
@ -67,7 +68,7 @@ test.describe('repo branch protection settings', () => {
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(3).fill('https://gitea.com/');
await save_visual(page);
await screenshot(page);
await page.click('.button.small.primary');
// Validate release page and click edit
@ -77,7 +78,7 @@ test.describe('repo branch protection settings', () => {
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) a')).toHaveAttribute('href', 'https://gitea.com/');
await save_visual(page);
await screenshot(page);
await page.locator('.octicon-pencil').first().click();
});

View file

@ -11,7 +11,8 @@
// @watch end
import {expect, type Page} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
import {accessibilityCheck} from './shared/accessibility.ts';
async function assertSelectedLines(page: Page, nums: string[]) {
@ -55,7 +56,7 @@ test('Line Range Selection', async ({page}) => {
// out-of-bounds end line
await page.goto(`${filePath}#L1-L100`);
await assertSelectedLines(page, ['1', '2', '3']);
await save_visual(page);
await screenshot(page);
});
test('Readable diff', async ({page}, workerInfo) => {
@ -82,7 +83,7 @@ test('Readable diff', async ({page}, workerInfo) => {
await expect(page.getByText(thisDiff.added, {exact: true})).toHaveCSS('background-color', 'rgb(134, 239, 172)');
}
}
await save_visual(page);
await screenshot(page);
});
test.describe('As authenticated user', () => {
@ -95,14 +96,14 @@ test.describe('As authenticated user', () => {
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);
await screenshot(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);
await screenshot(page);
});
});

View file

@ -5,7 +5,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test('Commit graph overflow', async ({page}) => {
const response = await page.goto('/user2/repo1/graph');
@ -28,7 +29,7 @@ test('Commit graph overflow', async ({page}) => {
await expect(page.getByRole('button', {name: 'Mono'})).toBeInViewport({ratio: 1});
await expect(page.getByRole('button', {name: 'Color'})).toBeInViewport({ratio: 1});
await expect(page.locator('.selection.search.dropdown')).toBeInViewport({ratio: 1});
await save_visual(page);
await screenshot(page);
});
test('Switch branch', async ({page}) => {
@ -45,5 +46,5 @@ test('Switch branch', async ({page}) => {
await expect(page.locator('#loading-indicator')).toBeHidden();
await expect(page.locator('#rel-container')).toBeVisible();
await expect(page.locator('#rev-container')).toBeVisible();
await save_visual(page);
await screenshot(page);
});

View file

@ -5,7 +5,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test('Language stats bar', async ({page}) => {
const response = await page.goto('/user2/language-stats-test');
@ -15,11 +16,11 @@ test('Language stats bar', async ({page}) => {
await page.click('#language-stats-bar');
await expect(page.locator('#language-stats-legend')).toBeVisible();
await save_visual(page);
await screenshot(page);
await page.click('#language-stats-bar');
await expect(page.locator('#language-stats-legend')).toBeHidden();
await save_visual(page);
await screenshot(page);
});
test('Branch selector commit icon', async ({page}) => {

View file

@ -3,7 +3,8 @@
// @watch end
import {expect} from '@playwright/test';
import {test, save_visual, test_context, dynamic_id} from './utils_e2e.ts';
import {test, test_context, dynamic_id} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
@ -22,7 +23,7 @@ test('Migration type seleciton screen', async ({page}) => {
await expect(page.locator('svg.gitea-gitbucket')).toBeVisible();
await expect(page.locator('svg.gitea-codebase')).toBeVisible();
await save_visual(page);
await screenshot(page);
});
test('Migration Repo Name detection', async ({page}, workerInfo) => {
@ -50,7 +51,7 @@ test('Migration Repo Name detection', async ({page}, workerInfo) => {
await expect(form.getByRole('textbox', {name: 'Repository Name'})).toHaveValue('test');
// Save screenshot only once
await save_visual(page);
await screenshot(page);
});
test('Migration Progress Page', async ({page, browser}, workerInfo) => {
@ -64,10 +65,10 @@ test('Migration Progress Page', async ({page, browser}, workerInfo) => {
const form = page.locator('form');
await form.getByRole('textbox', {name: 'Repository Name'}).fill(repoName);
await form.getByRole('textbox', {name: 'Migrate / Clone from URL'}).fill(`https://codeberg.org/forgejo/${repoName}`);
await save_visual(page);
await screenshot(page);
await form.locator('button.primary').click({timeout: 5000});
await expect(page).toHaveURL(`user2/${repoName}`);
await save_visual(page);
await screenshot(page);
const ctx = await test_context(browser, {storageState: {cookies: [], origins: []}});
const unauthenticatedPage = await ctx.newPage();
@ -76,13 +77,13 @@ test('Migration Progress Page', async ({page, browser}, workerInfo) => {
await page.reload();
await expect(page.locator('#repo_migrating_failed')).toBeVisible();
await save_visual(page);
await screenshot(page);
await page.getByRole('button', {name: 'Delete this repository'}).click();
const deleteModal = page.locator('#delete-repo-modal');
await deleteModal.getByRole('textbox', {name: 'Confirmation string'}).fill(`user2/${repoName}`);
await save_visual(page);
await screenshot(page);
await deleteModal.getByRole('button', {name: 'Delete repository'}).click();
await expect(page).toHaveURL('/');
// checked last to preserve the order of screenshots from first run
await save_visual(unauthenticatedPage);
await screenshot(unauthenticatedPage);
});

View file

@ -4,7 +4,8 @@
// @watch end
import {expect} from '@playwright/test';
import {test, dynamic_id, save_visual} from './utils_e2e.ts';
import {test, dynamic_id} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
import {validate_form} from './shared/forms.ts';
test.use({user: 'user2'});
@ -17,12 +18,12 @@ test('New repo: invalid', async ({page}) => {
await expect(page.getByText('.gitignore Select .gitignore')).toBeHidden();
await expect(page.getByText('Labels Select a label set')).toBeHidden();
await validate_form({page}, 'fieldset');
await save_visual(page);
await screenshot(page);
await page.getByLabel('Repository name').fill('*invalid');
await page.getByRole('button', {name: 'Create repository'}).click();
await expect(page.getByText('Repository name should contain only alphanumeric')).toBeVisible();
await save_visual(page);
await screenshot(page);
});
test('New repo: initialize', async ({page}, workerInfo) => {
@ -46,7 +47,7 @@ test('New repo: initialize', async ({page}, workerInfo) => {
await page.getByLabel('Make repository a template').check();
await validate_form({page}, 'fieldset');
await save_visual(page);
await screenshot(page);
const reponame = dynamic_id();
await page.getByLabel('Repository name').fill(reponame);
await page.getByRole('button', {name: 'Create repository'}).click();
@ -55,7 +56,7 @@ test('New repo: initialize', async ({page}, workerInfo) => {
if (!workerInfo.project.name.includes('Mobile')) {
await expect(page.getByText('Template', {exact: true})).toBeVisible();
}
await save_visual(page);
await screenshot(page);
});
test('New repo: initialize later', async ({page}) => {
@ -72,7 +73,7 @@ test('New repo: initialize later', async ({page}) => {
expect(page.url()).toBe(`http://localhost:3003/user2/${reponame}`);
await expect(page.getByRole('link', {name: 'New file'})).toBeVisible();
await expect(page.getByRole('heading', {name: 'Creating a new repository on'})).toBeVisible();
await save_visual(page);
await screenshot(page);
// add a README
await page.getByRole('link', {name: 'New file'}).click();
@ -89,7 +90,7 @@ test('New repo: initialize later', async ({page}) => {
expect(page.url()).toBe(`http://localhost:3003/user2/${reponame}/src/branch/devbranch/README.md`);
await expect(page.getByRole('link', {name: 'My first commit message'})).toBeVisible();
await expect(page.getByText('Hello Forgejo!')).toBeVisible();
await save_visual(page);
await screenshot(page);
});
test('New repo: from template', async ({page}, workerInfo) => {
@ -101,11 +102,11 @@ test('New repo: from template', async ({page}, workerInfo) => {
await page.getByRole('group', {name: 'Use a template You can select'}).getByRole('combobox').click();
await page.getByRole('option', {name: 'user27/template1'}).click();
await page.getByText('Git content (Default branch)').click();
await save_visual(page);
await screenshot(page);
await page.getByLabel('Repository name').fill(reponame);
await page.getByRole('button', {name: 'Create repository'}).click();
await expect(page.getByRole('link', {name: `${reponame}.log`})).toBeVisible();
await save_visual(page);
await screenshot(page);
});
test('New repo: label set', async ({page}) => {
@ -117,13 +118,13 @@ test('New repo: label set', async ({page}) => {
await page.getByRole('option', {name: 'Advanced (Kind/Bug, Kind/'}).click();
// close dropdown via unrelated click
await page.getByText('You can select an existing').click();
await save_visual(page);
await screenshot(page);
await page.getByLabel('Repository name').fill(reponame);
await page.getByRole('button', {name: 'Create repository'}).click();
await page.goto(`/user2/${reponame}/issues`);
await page.getByRole('link', {name: 'Labels'}).click();
await expect(page.getByText('Kind/Bug Something is not')).toBeVisible();
await save_visual(page);
await screenshot(page);
});
test('New repo: gitignore', async ({page}) => {
@ -140,7 +141,7 @@ test('New repo: gitignore', async ({page}) => {
await page.getByRole('option', {name: 'NotesAndExtendedConfiguration'}).click();
await page.getByRole('option', {name: 'MetaProgrammingSystem'}).click();
await page.getByRole('option', {name: 'AppceleratorTitanium'}).click();
await save_visual(page);
await screenshot(page);
const segmentWidth = (await page.locator('.segment').boundingBox()).width;
const dropdownWidth = (await gitignoreDropdown.boundingBox()).width;

View file

@ -7,7 +7,8 @@
// @watch end
import {expect} from '@playwright/test';
import {test, save_visual} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
import {validate_form} from './shared/forms.ts';
test.use({user: 'user2'});
@ -21,13 +22,13 @@ test('repo webhook settings', async ({page}) => {
// check accessibility including the custom events (now visible) part
await validate_form({page}, 'fieldset');
await save_visual(page);
await screenshot(page);
await page.locator('input[name="events"][value="push_only"]').click();
await expect(page.locator('.hide-unless-checked')).toBeHidden();
await page.locator('input[name="events"][value="send_everything"]').click();
await expect(page.locator('.hide-unless-checked')).toBeHidden();
await save_visual(page);
await screenshot(page);
});
test.describe('repo branch protection settings', () => {
@ -53,11 +54,11 @@ test.describe('repo branch protection settings', () => {
// verify header is new
await expect(page.locator('h4')).toContainText('new');
await page.locator('input[name="rule_name"]').fill('testrule');
await save_visual(page);
await screenshot(page);
await page.locator('button:text("Save rule")').click();
// verify header is in edit mode
await page.waitForLoadState('domcontentloaded');
await save_visual(page);
await screenshot(page);
// find the edit button and click it
const editButton = page.locator('a[href="/user2/repo1/settings/branches/edit?rule_name=testrule"]');
@ -65,6 +66,6 @@ test.describe('repo branch protection settings', () => {
await page.waitForLoadState();
await expect(page.locator('.repo-setting-content .header')).toContainText('Protection rules for branch', {ignoreCase: true, useInnerText: true});
await save_visual(page);
await screenshot(page);
});
});

View file

@ -4,7 +4,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
for (const searchTerm of ['space', 'consectetur']) {
for (const width of [null, 2560, 4000]) {
@ -25,7 +26,7 @@ for (const searchTerm of ['space', 'consectetur']) {
await expect(page.locator('#wiki-search a[href]')).toBeInViewport({
ratio: workerInfo.project.name === 'webkit' ? 0.9 : 1,
});
await save_visual(page);
await screenshot(page);
});
}
}
@ -39,12 +40,12 @@ test(`Search results show titles (and not file names)`, async ({page}, workerInf
// so we manually "type" the last letter
await page.getByPlaceholder('Search wiki').dispatchEvent('keyup');
await expect(page.locator('#wiki-search a[href] b')).toHaveText('Page With Spaced Name');
await save_visual(page);
await screenshot(page);
});
test('Wiki unicode-escape', async ({page}) => {
await page.goto('/user2/unicode-escaping/wiki');
await save_visual(page);
await screenshot(page);
expect(await page.locator('.ui.message.unicode-escape-prompt').count()).toEqual(3);

View file

@ -5,7 +5,8 @@
// @watch end
import {expect} from '@playwright/test';
import {save_visual, test} from './utils_e2e.ts';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.describe('desktop viewport as user 2', () => {
test.use({user: 'user2', viewport: {width: 1920, height: 300}});
@ -54,7 +55,7 @@ test.describe('desktop viewport, unauthenticated', () => {
await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0);
await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
await save_visual(page);
await screenshot(page);
});
});
@ -79,7 +80,7 @@ test.describe('small viewport', () => {
const items = shownItems.concat(overflowItems);
expect(Array.from(new Set(items))).toHaveLength(items.length);
await save_visual(page);
await screenshot(page);
});
test('Settings button in overflow menu of org header', async ({page}) => {
@ -123,6 +124,6 @@ test.describe('small viewport, unauthenticated', () => {
const items = shownItems.concat(overflowItems);
expect(Array.from(new Set(items))).toHaveLength(items.length);
await save_visual(page);
await screenshot(page);
});
});

View file

@ -0,0 +1,89 @@
import {expect, type Page, type Locator} from '@playwright/test';
// returns element that should be covered before taking the screenshot
async function masks(page: Page) : Array<Locator> {
return [
page.locator('.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'}),
page.locator('#activity-feed'),
page.locator('#user-heatmap'),
// dynamic IDs in fixed-size inputs
page.locator('input[value*="dyn-id-"]'),
];
}
// replaces elements on the page that cause flakiness
async function screenshot_prepare(page: Page) {
await page.waitForLoadState('domcontentloaded');
// Version string is dynamic
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';
});
// other time elements
await page.locator('relative-time').evaluateAll((nodes) => {
for (const node of nodes) node.outerHTML = 'time element';
});
await page.locator('absolute-date').evaluateAll((nodes) => {
for (const node of nodes) node.outerHTML = 'time element';
});
// dynamically generated UUIDs
await page.getByText('dyn-id-').evaluateAll((nodes) => {
for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id');
});
// repeat above, work around https://github.com/microsoft/playwright/issues/34152
await page.getByText('dyn-id-').evaluateAll((nodes) => {
for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id');
});
// attachment IDs in text areas, required for issue-comment-dropzone.
// playwright does not (yet?) support filtering for content in input elements, see https://github.com/microsoft/playwright/issues/36166
await page.locator('textarea.markdown-text-editor').evaluateAll((nodes) => {
for (const node of nodes) node.value = node.value.replaceAll(/attachments\/[a-f0-9-]+/g, '/attachments/c1ee9740-dad3-4747-b489-f6fb2e3dfcec');
});
// dynamically created test users
await page.getByText('e2e-test-').evaluateAll((nodes) => {
for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/e2e-test-[0-9-]+/g, 'e2e-test-user');
});
}
export async function screenshot(page: Page, locator?: Locator, margin = 0) {
// Optionally include visual testing
if (process.env.VISUAL_TEST) {
await screenshot_prepare(page);
if (locator === undefined) {
await screenshot_full(page);
} else {
await screenshot_selective(page, locator, margin);
}
}
}
async function screenshot_selective(page: Page, locator: Locator, margin: number) {
const clip = await locator.boundingBox();
clip.x = Math.max(clip.x - margin, 0);
clip.y = Math.max(clip.y - margin, 0);
clip.width += margin * 2;
clip.height += margin * 2;
await expect(page).toHaveScreenshot({
fullPage: true,
timeout: 20000,
clip,
mask: await masks(page),
});
}
async function screenshot_full(page: Page) {
await expect(page).toHaveScreenshot({
fullPage: true,
timeout: 20000,
mask: await masks(page),
});
}

View file

@ -4,7 +4,8 @@
// @watch end
import {expect} from '@playwright/test';
import {test, save_visual, login_user, login} from './utils_e2e.ts';
import {test, login_user, login} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
import {validate_form} from './shared/forms.ts';
test.beforeAll(async ({browser}, workerInfo) => {
@ -36,12 +37,12 @@ test('User: Profile settings', async ({browser}, workerInfo) => {
await page.getByLabel('Hide activity from profile').check();
await validate_form({page}, 'fieldset');
await save_visual(page);
await screenshot(page);
await page.getByRole('button', {name: 'Update profile'}).click();
await expect(page.getByText('Your profile has been updated.')).toBeVisible();
await page.getByRole('link', {name: 'public activity'}).click();
await expect(page.getByText('Your activity is only visible')).toBeVisible();
await save_visual(page);
await screenshot(page);
await page.goto('/user2');
await expect(page.getByText('SecondUser')).toBeVisible();
@ -49,7 +50,7 @@ test('User: Profile settings', async ({browser}, workerInfo) => {
await expect(page.locator('li').filter({hasText: 'user2@example.com'})).toBeVisible();
await expect(page.locator('li').filter({hasText: 'https://forgejo.org'})).toBeVisible();
await expect(page.getByText('I am a playwright test')).toBeVisible();
await save_visual(page);
await screenshot(page);
await page.goto('/user/settings');
await page.locator('input[list="pronouns"]').fill('rob/ot');
@ -59,7 +60,7 @@ test('User: Profile settings', async ({browser}, workerInfo) => {
await page.getByLabel('Hide activity from profile').uncheck();
await expect(page.getByText('Your profile has been updated.')).toBeHidden();
await validate_form({page}, 'fieldset');
await save_visual(page);
await screenshot(page);
await page.getByRole('button', {name: 'Update profile'}).click();
await expect(page.getByText('Your profile has been updated.')).toBeVisible();
@ -84,13 +85,13 @@ test('User: Storage overview', async ({browser}, workerInfo) => {
await page.locator('.stats summary').nth(1).click();
await expect(page.locator('.stats ul').nth(1)).toBeVisible();
await expect(page.getByText('Git LFS 8 KiB').nth(1)).toBeVisible();
await save_visual(page);
await screenshot(page);
await page.locator('.stats summary').nth(1).click();
await expect(page.locator('.stats ul').nth(1)).toBeHidden();
await expect(page.getByText('Git LFS 8 KiB').nth(1)).toBeHidden();
await save_visual(page);
await screenshot(page);
});
test('User: Canceling adding SSH key clears inputs', async ({browser}, workerInfo) => {

View file

@ -1,4 +1,4 @@
import {expect, test as baseTest, type Browser, type BrowserContextOptions, type APIRequestContext, type TestInfo, type Page} from '@playwright/test';
import {expect, test as baseTest, type Browser, type BrowserContextOptions, type APIRequestContext, type TestInfo} from '@playwright/test';
import * as path from 'node:path';
@ -84,49 +84,6 @@ export async function login({browser}: {browser: Browser}, workerInfo: TestInfo)
return await context?.newPage();
}
export async function save_visual(page: Page) {
// Optionally include visual testing
if (process.env.VISUAL_TEST) {
await page.waitForLoadState('domcontentloaded');
// Mock/replace dynamic content which can have different size (and thus cannot simply be masked below)
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';
});
// dynamically generated UUIDs
await page.getByText('dyn-id-').evaluateAll((nodes) => {
for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id');
});
// repeat above, work around https://github.com/microsoft/playwright/issues/34152
await page.getByText('dyn-id-').evaluateAll((nodes) => {
for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id');
});
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({
fullPage: true,
timeout: 20000,
mask: [
page.locator('.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'}),
page.locator('#activity-feed'),
page.locator('#user-heatmap'),
// dynamic IDs in fixed-size inputs
page.locator('input[value*="dyn-id-"]'),
],
});
}
}
// Create a temporary user and login to that user and store session info.
// This should ideally run on a per test basis.
export async function create_temp_user(browser: Browser, workerInfo: TestInfo, request: APIRequestContext) {

View file

@ -8,7 +8,8 @@
// @watch end
import {expect} from '@playwright/test';
import {test, save_visual, create_temp_user, login_user} from './utils_e2e.ts';
import {test, create_temp_user, login_user} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => {
test.skip(workerInfo.project.name !== 'chromium', 'Uses Chrome protocol');
@ -34,7 +35,7 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) =>
});
await page.locator('input#nickname').fill('Testing Security Key');
await save_visual(page);
await screenshot(page, page.locator('.user-setting-content'));
await page.getByText('Add security key').click();
// Logout.
@ -57,7 +58,7 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) =>
response = await page.goto('/user/settings/security');
expect(response?.status()).toBe(200);
await page.getByRole('button', {name: 'Remove'}).click();
await save_visual(page);
await screenshot(page, page.locator('.ui.g-modal-confirm.delete.modal'), 50);
await page.getByRole('button', {name: 'Yes'}).click();
await page.waitForLoadState();