diff --git a/tests/e2e/actions.test.e2e.ts b/tests/e2e/actions.test.e2e.ts index e7fef6bc0a..083b25a7b9 100644 --- a/tests/e2e/actions.test.e2e.ts +++ b/tests/e2e/actions.test.e2e.ts @@ -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) => { diff --git a/tests/e2e/clipboard-copy.test.e2e.ts b/tests/e2e/clipboard-copy.test.e2e.ts index 2517d07463..2d159e8e90 100644 --- a/tests/e2e/clipboard-copy.test.e2e.ts +++ b/tests/e2e/clipboard-copy.test.e2e.ts @@ -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); }); diff --git a/tests/e2e/dashboard-ci-status.test.e2e.ts b/tests/e2e/dashboard-ci-status.test.e2e.ts index d35fe299ff..06c8147770 100644 --- a/tests/e2e/dashboard-ci-status.test.e2e.ts +++ b/tests/e2e/dashboard-ci-status.test.e2e.ts @@ -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); }); diff --git a/tests/e2e/dimmer.test.e2e.ts b/tests/e2e/dimmer.test.e2e.ts index ed8d116e1f..ffdbf7e20b 100644 --- a/tests/e2e/dimmer.test.e2e.ts +++ b/tests/e2e/dimmer.test.e2e.ts @@ -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(() => { diff --git a/tests/e2e/example.test.e2e.ts b/tests/e2e/example.test.e2e.ts index 97c5b8684b..8d94fa88d7 100644 --- a/tests/e2e/example.test.e2e.ts +++ b/tests/e2e/example.test.e2e.ts @@ -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 diff --git a/tests/e2e/explore.test.e2e.ts b/tests/e2e/explore.test.e2e.ts index 1bb5af3cc6..f953ba2d71 100644 --- a/tests/e2e/explore.test.e2e.ts +++ b/tests/e2e/explore.test.e2e.ts @@ -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); }); diff --git a/tests/e2e/git-notes.test.e2e.ts b/tests/e2e/git-notes.test.e2e.ts index 1e2cbe76fc..f1f4d07c1d 100644 --- a/tests/e2e/git-notes.test.e2e.ts +++ b/tests/e2e/git-notes.test.e2e.ts @@ -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')); }); diff --git a/tests/e2e/image-diff.test.e2e.ts b/tests/e2e/image-diff.test.e2e.ts index f7d4f7bd69..273564fcb3 100644 --- a/tests/e2e/image-diff.test.e2e.ts +++ b/tests/e2e/image-diff.test.e2e.ts @@ -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')); }); diff --git a/tests/e2e/issue-comment-dropzone.test.e2e.ts b/tests/e2e/issue-comment-dropzone.test.e2e.ts index 33ea2c9403..adbdb69f87 100644 --- a/tests/e2e/issue-comment-dropzone.test.e2e.ts +++ b/tests/e2e/issue-comment-dropzone.test.e2e.ts @@ -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')); }); diff --git a/tests/e2e/issue-comment.test.e2e.ts b/tests/e2e/issue-comment.test.e2e.ts index e103b84774..726b1d167a 100644 --- a/tests/e2e/issue-comment.test.e2e.ts +++ b/tests/e2e/issue-comment.test.e2e.ts @@ -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) => { diff --git a/tests/e2e/issue-sidebar.test.e2e.ts b/tests/e2e/issue-sidebar.test.e2e.ts index f181185d69..f8f0fadbd5 100644 --- a/tests/e2e/issue-sidebar.test.e2e.ts +++ b/tests/e2e/issue-sidebar.test.e2e.ts @@ -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', () => { diff --git a/tests/e2e/issue-timetracking.test.e2e.ts b/tests/e2e/issue-timetracking.test.e2e.ts index 901cbe793f..1cb65190bf 100644 --- a/tests/e2e/issue-timetracking.test.e2e.ts +++ b/tests/e2e/issue-timetracking.test.e2e.ts @@ -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(); diff --git a/tests/e2e/login.test.e2e.ts b/tests/e2e/login.test.e2e.ts index 01cf4d7b8d..465b9577d4 100644 --- a/tests/e2e/login.test.e2e.ts +++ b/tests/e2e/login.test.e2e.ts @@ -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.'); diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts index ae79b75800..6c5e0df378 100644 --- a/tests/e2e/markdown-editor.test.e2e.ts +++ b/tests/e2e/markdown-editor.test.e2e.ts @@ -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}) => { diff --git a/tests/e2e/markup.test.e2e.ts b/tests/e2e/markup.test.e2e.ts index b26e83661b..a5e859f677 100644 --- a/tests/e2e/markup.test.e2e.ts +++ b/tests/e2e/markup.test.e2e.ts @@ -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); }); diff --git a/tests/e2e/modal.test.e2e.ts b/tests/e2e/modal.test.e2e.ts index dbcfcb3ea4..946a7f6d46 100644 --- a/tests/e2e/modal.test.e2e.ts +++ b/tests/e2e/modal.test.e2e.ts @@ -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(); diff --git a/tests/e2e/org-settings.test.e2e.ts b/tests/e2e/org-settings.test.e2e.ts index df554e0674..eecd769f70 100644 --- a/tests/e2e/org-settings.test.e2e.ts +++ b/tests/e2e/org-settings.test.e2e.ts @@ -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}); diff --git a/tests/e2e/pr-review.test.e2e.ts b/tests/e2e/pr-review.test.e2e.ts index 4e95e0aa69..22f3971202 100644 --- a/tests/e2e/pr-review.test.e2e.ts +++ b/tests/e2e/pr-review.test.e2e.ts @@ -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', diff --git a/tests/e2e/pr-title.test.e2e.ts b/tests/e2e/pr-title.test.e2e.ts index 390cc81298..2d2c108cb5 100644 --- a/tests/e2e/pr-title.test.e2e.ts +++ b/tests/e2e/pr-title.test.e2e.ts @@ -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); }); diff --git a/tests/e2e/profile_actions.test.e2e.ts b/tests/e2e/profile_actions.test.e2e.ts index e27ecf64cf..5935de5e7f 100644 --- a/tests/e2e/profile_actions.test.e2e.ts +++ b/tests/e2e/profile_actions.test.e2e.ts @@ -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(); diff --git a/tests/e2e/reaction-selectors.test.e2e.ts b/tests/e2e/reaction-selectors.test.e2e.ts index 54b7d91869..598369ffa3 100644 --- a/tests/e2e/reaction-selectors.test.e2e.ts +++ b/tests/e2e/reaction-selectors.test.e2e.ts @@ -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); }); diff --git a/tests/e2e/release.test.e2e.ts b/tests/e2e/release.test.e2e.ts index a4303a7320..3d37b34e77 100644 --- a/tests/e2e/release.test.e2e.ts +++ b/tests/e2e/release.test.e2e.ts @@ -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(); }); diff --git a/tests/e2e/repo-code.test.e2e.ts b/tests/e2e/repo-code.test.e2e.ts index 9f885958cb..26ae034b09 100644 --- a/tests/e2e/repo-code.test.e2e.ts +++ b/tests/e2e/repo-code.test.e2e.ts @@ -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); }); }); diff --git a/tests/e2e/repo-commitgraph.test.e2e.ts b/tests/e2e/repo-commitgraph.test.e2e.ts index e8b85c5997..716b77adee 100644 --- a/tests/e2e/repo-commitgraph.test.e2e.ts +++ b/tests/e2e/repo-commitgraph.test.e2e.ts @@ -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); }); diff --git a/tests/e2e/repo-home.test.e2e.ts b/tests/e2e/repo-home.test.e2e.ts index 6f3d6c373b..52281b1560 100644 --- a/tests/e2e/repo-home.test.e2e.ts +++ b/tests/e2e/repo-home.test.e2e.ts @@ -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}) => { diff --git a/tests/e2e/repo-migrate.test.e2e.ts b/tests/e2e/repo-migrate.test.e2e.ts index f0be73e777..26619547ae 100644 --- a/tests/e2e/repo-migrate.test.e2e.ts +++ b/tests/e2e/repo-migrate.test.e2e.ts @@ -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); }); diff --git a/tests/e2e/repo-new.test.e2e.ts b/tests/e2e/repo-new.test.e2e.ts index ed5e0beb00..bbe0652fba 100644 --- a/tests/e2e/repo-new.test.e2e.ts +++ b/tests/e2e/repo-new.test.e2e.ts @@ -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; diff --git a/tests/e2e/repo-settings.test.e2e.ts b/tests/e2e/repo-settings.test.e2e.ts index 547b015921..aa57c45e8e 100644 --- a/tests/e2e/repo-settings.test.e2e.ts +++ b/tests/e2e/repo-settings.test.e2e.ts @@ -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); }); }); diff --git a/tests/e2e/repo-wiki.test.e2e.ts b/tests/e2e/repo-wiki.test.e2e.ts index 61bb2ad181..02ada9e6cc 100644 --- a/tests/e2e/repo-wiki.test.e2e.ts +++ b/tests/e2e/repo-wiki.test.e2e.ts @@ -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); diff --git a/tests/e2e/right-settings-button.test.e2e.ts b/tests/e2e/right-settings-button.test.e2e.ts index 3bea329ba0..8daeddacf2 100644 --- a/tests/e2e/right-settings-button.test.e2e.ts +++ b/tests/e2e/right-settings-button.test.e2e.ts @@ -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); }); }); diff --git a/tests/e2e/shared/screenshots.ts b/tests/e2e/shared/screenshots.ts new file mode 100644 index 0000000000..de5a34d1fb --- /dev/null +++ b/tests/e2e/shared/screenshots.ts @@ -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 { + 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), + }); +} diff --git a/tests/e2e/user-settings.test.e2e.ts b/tests/e2e/user-settings.test.e2e.ts index 5abe6878e3..db3ef55b3c 100644 --- a/tests/e2e/user-settings.test.e2e.ts +++ b/tests/e2e/user-settings.test.e2e.ts @@ -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) => { diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts index ff921a2cf3..e2d56691e6 100644 --- a/tests/e2e/utils_e2e.ts +++ b/tests/e2e/utils_e2e.ts @@ -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) { diff --git a/tests/e2e/webauthn.test.e2e.ts b/tests/e2e/webauthn.test.e2e.ts index d4b81621d2..1e83c409dc 100644 --- a/tests/e2e/webauthn.test.e2e.ts +++ b/tests/e2e/webauthn.test.e2e.ts @@ -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();