forgejo/tests/e2e/issue-comment.test.e2e.ts
Gusted 0737196842 chore: remove webkit and mobile safari from playwright (#10103)
Webkit and Mobile safari are comically unreliable, will fail for unexplainable reasons and are very hard to run locally in comparison with the other supported platforms. I do not remember the last time where these two platforms were able to catch a regression where the other platforms did not.

I would like to stress, for the historical record, that many hours has been devoted into adjusting the tests and following best practices to make these two platforms more stable but despite those, IMO wasted, efforts these two platforms are causing many hours of wasted CPU time simply because they are flaky and make (new) contributors nervous if their change contains a regression or not.

To my knowledge, the tests are not broken for these two platforms. If you go to the issue tracker you will not find issues by users that use these two platforms and report that Forgejo is broken. It does not reflect reality.

This is the sunk cost fallacy, bite the bullet and agree that these platforms will not contribute positively to Forgejo's excellent test suite.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10103
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
2025-11-13 17:23:08 +01:00

410 lines
19 KiB
TypeScript

// @watch start
// web_src/js/features/comp/**
// web_src/js/features/repo-**
// templates/repo/issue/view_content/*
// routers/web/repo/issue_content_history.go
// @watch end
import {expect} from '@playwright/test';
import {test, dynamic_id, login_user} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
for (const run of [
{title: 'JS off', js: true},
{title: 'JS on', js: false},
]) {
test.describe(`Create issue & comment`, () => {
// playwright/valid-title says: [error] Title must be a string
test(`${run.title}`, async ({browser}, workerInfo) => {
test.skip(['Mobile Chrome'].includes(workerInfo.project.name), 'Mobile Chrome has trouble clicking Comment button with JS enabled');
const issueTitle = dynamic_id();
const issueContent = dynamic_id();
const commentContent = dynamic_id();
const context = await login_user(browser, workerInfo, 'user2', {javaScriptEnabled: run.js});
const page = await context.newPage();
let response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
// Create a new issue
await page.getByPlaceholder('Title').fill(issueTitle);
await page.getByPlaceholder('Leave a comment').fill(issueContent);
await page.getByRole('button', {name: 'Create issue'}).click();
if (run.js) {
await expect(page).toHaveURL(/\/user2\/repo1\/issues\/\d+$/);
} else {
// NoJS clients end up on a .../comments JSON file and browsers surround it with some HTML
const redirectUrl = await JSON.parse(await page.locator('body').textContent())['redirect'];
response = await page.goto(redirectUrl);
expect(response?.status()).toBe(200);
}
// Leave a comment
await page.locator('#comment-form').getByPlaceholder('Leave a comment').fill(commentContent);
await page.locator('#comment-form button.primary').filter({hasText: 'Comment'}).click();
if (!run.js) {
const redirectUrl = await JSON.parse(await page.locator('body').textContent())['redirect'];
response = await page.goto(redirectUrl);
expect(response?.status()).toBe(200);
}
// Validate the page contents that actions above made a difference
await expect(page.locator('h1')).toContainText(issueTitle);
await expect(page.locator('.comment').filter({hasText: issueContent})).toHaveCount(1);
await expect(page.locator('.comment').filter({hasText: commentContent})).toHaveCount(1);
});
});
}
test('Menu accessibility', async ({page}) => {
await page.goto('/user2/repo1/issues/1');
await expect(page.getByLabel('user2 reacted eyes. Remove eyes')).toBeVisible();
await expect(page.getByLabel('reacted laugh. Remove laugh')).toBeVisible();
await expect(page.locator('#issue-1').getByLabel('Comment menu')).toBeVisible();
await expect(page.locator('#issue-1').getByRole('heading').getByLabel('Add reaction')).toBeVisible();
page.getByLabel('reacted laugh. Remove').click();
await expect(page.getByLabel('user1 reacted laugh. Add laugh')).toBeVisible();
page.getByLabel('user1 reacted laugh.').click();
await expect(page.getByLabel('user1, user2 reacted laugh. Remove laugh')).toBeVisible();
});
test.describe('Button text replaced by JS', () => {
async function testPage(page, path, closeLabel) {
await page.goto(path);
const statusButton = page.locator('#status-button');
const statusButtonIcon = page.locator('#status-button svg');
const commentField = page.locator('#comment-form').getByPlaceholder('Leave a comment');
// Reset issue status before running the test
if (await statusButton.getByText('Reopen').isVisible()) await statusButton.click();
// Assert that normal Close button text is present
await expect(statusButton.getByText(closeLabel)).toBeVisible();
await expect(statusButtonIcon).toBeVisible();
// Type in some text to make button text change
await commentField.fill('Blah blah');
await expect(statusButton.getByText('Close with comment')).toBeVisible();
await expect(statusButtonIcon).toBeVisible();
// Close issue/PR and assert that normal Reopen button text is present
await statusButton.click();
await expect(statusButton.getByText('Reopen')).toBeVisible();
await expect(statusButtonIcon).toBeVisible();
// Type in some text to make button text change
await commentField.fill('Blah blah');
await expect(statusButton.getByText('Reopen with comment')).toBeVisible();
await expect(statusButtonIcon).toBeVisible();
return true;
}
test('Issue', async ({page}) => {
// All actual expect() are happening in the helper
expect(await testPage(page, '/user2/repo1/issues/1', 'Close issue')).toBeTruthy();
});
test('PR', async ({page}) => {
expect(await testPage(page, '/user2/repo1/pulls/3', 'Close pull request')).toBeTruthy();
});
});
test('Hyperlink paste behaviour', async ({page, isMobile}) => {
test.skip(isMobile, 'Mobile clients seem to have very weird behaviour with this test, which I cannot confirm with real usage');
await page.goto('/user2/repo1/issues/new');
await page.locator('textarea').click();
// same URL
await page.locator('textarea').fill('https://codeberg.org/forgejo/forgejo#some-anchor');
await page.locator('textarea').press('Shift+Home');
await page.locator('textarea').press('ControlOrMeta+c');
await page.locator('textarea').press('ControlOrMeta+v');
await expect(page.locator('textarea')).toHaveValue('https://codeberg.org/forgejo/forgejo#some-anchor');
// other text
await page.locator('textarea').fill('Some other text');
await page.locator('textarea').press('ControlOrMeta+a');
await page.locator('textarea').press('ControlOrMeta+v');
await expect(page.locator('textarea')).toHaveValue('[Some other text](https://codeberg.org/forgejo/forgejo#some-anchor)');
// subset of URL
await page.locator('textarea').fill('https://codeberg.org/forgejo/forgejo#some');
await page.locator('textarea').press('ControlOrMeta+a');
await page.locator('textarea').press('ControlOrMeta+v');
await expect(page.locator('textarea')).toHaveValue('https://codeberg.org/forgejo/forgejo#some-anchor');
// superset of URL
await page.locator('textarea').fill('https://codeberg.org/forgejo/forgejo#some-anchor-on-the-page');
await page.locator('textarea').press('ControlOrMeta+a');
await page.locator('textarea').press('ControlOrMeta+v');
await expect(page.locator('textarea')).toHaveValue('https://codeberg.org/forgejo/forgejo#some-anchor');
// completely separate URL
await page.locator('textarea').fill('http://example.com');
await page.locator('textarea').press('ControlOrMeta+a');
await page.locator('textarea').press('ControlOrMeta+v');
await expect(page.locator('textarea')).toHaveValue('https://codeberg.org/forgejo/forgejo#some-anchor');
await page.locator('textarea').fill('');
});
test('Always focus edit tab first on edit', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
// Switch to preview tab and save
await page.click('#issue-1 .comment-container .context-menu');
await page.click('#issue-1 .comment-container .menu>.edit-content');
await page.locator('#issue-1 .comment-container [data-tab-for=markdown-previewer]').click();
await page.click('#issue-1 .comment-container .save');
await page.waitForLoadState();
// Edit again and assert that edit tab should be active (and not preview tab)
await page.click('#issue-1 .comment-container .context-menu');
await page.click('#issue-1 .comment-container .menu>.edit-content');
const editTab = page.locator('#issue-1 .comment-container [data-tab-for=markdown-writer]');
const previewTab = page.locator('#issue-1 .comment-container [data-tab-for=markdown-previewer]');
await expect(editTab).toHaveClass(/active/);
await expect(previewTab).not.toHaveClass(/active/);
await screenshot(page, page.locator('.issue-content-left'));
});
test('Reset content of comment edit field on cancel', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
const editorTextarea = page.locator('[id="_combo_markdown_editor_1"]');
// Change the content of the edit field
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 editorTextarea.fill('some random string');
await expect(editorTextarea).toHaveValue('some random string');
await page.click('#issue-1 .comment-container .edit .cancel');
// Edit again and assert that the edit field should be reset to the initial content
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 screenshot(page, page.locator('.issue-content-left'));
});
test('Quote reply', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name !== 'firefox', 'Uses Firefox specific selection quirks');
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
const editorTextarea = page.locator('textarea.markdown-text-editor');
// Full quote.
await page.click('#issuecomment-1001 .comment-container .context-menu');
await page.click('#issuecomment-1001 .quote-reply');
await expect(editorTextarea).toHaveValue('@user2 wrote in http://localhost:3003/user2/repo1/issues/1#issuecomment-1001:\n\n' +
'> ## [](#lorem-ipsum)Lorem Ipsum\n' +
'> \n' +
'> I would like to say that **I am not appealed** that it took _so long_ for this `feature` to be [created](https://example.com) \\(e^{\\pi i} + 1 = 0\\)\n' +
'> \n' +
'> \\[e^{\\pi i} + 1 = 0\\]\n' +
'> \n' +
'> #1\n' +
'> \n' +
'> ```js\n' +
"> console.log('evil')\n" +
"> alert('evil')\n" +
'> ```\n' +
'> \n' +
'> :+1: :100: [![hi there](/attachments/3f4f4016-877b-46b3-b79f-ad24519a9cf2)](/user2/repo1/attachments/3f4f4016-877b-46b3-b79f-ad24519a9cf2)\n' +
'> <img alt="something something" width="500" height="500" src="/attachments/3f4f4016-877b-46b3-b79f-ad24519a9cf2">\n\n');
await editorTextarea.fill('');
// Partial quote.
await page.click('#issuecomment-1001 .comment-container .context-menu');
await page.evaluate(() => {
const range = new Range();
range.setStart(document.querySelector('#issuecomment-1001-content #user-content-lorem-ipsum').childNodes[1], 6);
range.setEnd(document.querySelector('#issuecomment-1001-content p').childNodes[1].childNodes[0], 7);
const selection = window.getSelection();
// Add range to window selection
selection.addRange(range);
});
await page.click('#issuecomment-1001 .quote-reply');
await expect(editorTextarea).toHaveValue('@user2 wrote in http://localhost:3003/user2/repo1/issues/1#issuecomment-1001:\n\n' +
'> ## Ipsum\n' +
'> \n' +
'> I would like to say that **I am no**\n\n');
await editorTextarea.fill('');
// Another partial quote.
await page.click('#issuecomment-1001 .comment-container .context-menu');
await page.evaluate(() => {
const range = new Range();
range.setStart(document.querySelector('#issuecomment-1001-content p').childNodes[1].childNodes[0], 7);
range.setEnd(document.querySelector('#issuecomment-1001-content p').childNodes[7].childNodes[0], 3);
const selection = window.getSelection();
// Add range to window selection
selection.addRange(range);
});
await page.click('#issuecomment-1001 .quote-reply');
await expect(editorTextarea).toHaveValue('@user2 wrote in http://localhost:3003/user2/repo1/issues/1#issuecomment-1001:\n\n' +
'> **t appealed** that it took _so long_ for this `feature` to be [cre](https://example.com)\n\n');
await editorTextarea.fill('');
});
test('Pull quote reply', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name !== 'firefox', 'Uses Firefox specific selection quirks');
const response = await page.goto('/user2/commitsonpr/pulls/1/files');
expect(response?.status()).toBe(200);
const editorTextarea = page.locator('form.comment-form textarea.markdown-text-editor');
// Full quote with no reply handler being open.
await page.click('.comment-code-cloud .context-menu');
await page.click('.comment-code-cloud .quote-reply');
await expect(editorTextarea).toHaveValue('@user2 wrote in http://localhost:3003/user2/commitsonpr/pulls/1/files#issuecomment-1002:\n\n' +
'> ## [](#lorem-ipsum)Lorem Ipsum\n' +
'> \n' +
'> I would like to say that **I am not appealed** that it took _so long_ for this `feature` to be [created](https://example.com) \\(e^{\\pi i} + 1 = 0\\)\n' +
'> \n' +
'> \\[e^{\\pi i} + 1 = 0\\]\n' +
'> \n' +
'> #1\n' +
'> \n' +
'> ```js\n' +
"> console.log('evil')\n" +
"> alert('evil')\n" +
'> ```\n' +
'> \n' +
'> :+1: :100: [![hi there](/attachments/3f4f4016-877b-46b3-b79f-ad24519a9cf2)](/user2/commitsonpr/attachments/3f4f4016-877b-46b3-b79f-ad24519a9cf2)\n' +
'> <img alt="something something" width="500" height="500" src="/attachments/3f4f4016-877b-46b3-b79f-ad24519a9cf2">\n\n');
await editorTextarea.fill('');
});
test('Emoji suggestions', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
const textarea = page.locator('#comment-form textarea[name=content]');
await textarea.focus();
await textarea.pressSequentially(':');
const suggestionList = page.locator('#comment-form .suggestions');
await expect(suggestionList).toBeVisible();
const expectedSuggestions = [
{emoji: '👍', name: '+1'},
{emoji: '👎', name: '-1'},
{emoji: '💯', name: '100'},
{emoji: '🔢', name: '1234'},
{emoji: '🥇', name: '1st_place_medal'},
{emoji: '🥈', name: '2nd_place_medal'},
];
for (const {emoji, name} of expectedSuggestions) {
const item = suggestionList.locator(`li:has-text("${name}")`);
await expect(item).toContainText(`${emoji} ${name}`);
}
await textarea.pressSequentially('forge');
await expect(suggestionList).toBeVisible();
const item = suggestionList.locator(`li:has-text("forgejo")`);
await expect(item.locator('img')).toHaveAttribute('src', '/assets/img/emoji/forgejo.png');
});
test.describe('Comment history', () => {
let issueURL = '';
test('Deleted items in comment history menu', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
// Create a new issue.
await page.getByPlaceholder('Title').fill('Just a title');
await page.getByPlaceholder('Leave a comment').fill('Hi, have you considered using a rotating fish as logo?');
await page.getByRole('button', {name: 'Create issue'}).click();
await expect(page).toHaveURL(/\/user2\/repo1\/issues\/\d+$/);
issueURL = page.url();
page.on('dialog', (dialog) => dialog.accept());
// Make a change.
const editorTextarea = page.locator('[id="_combo_markdown_editor_1"]');
await page.click('.comment-container .context-menu');
await page.click('.comment-container .menu>.edit-content');
await editorTextarea.fill(dynamic_id());
await page.click('.comment-container .edit .save');
// Reload the page so the edited bit is rendered.
await page.reload();
await page.getByText('• edited').click();
await page.click('.content-history-menu .item:nth-child(1)');
await page.getByText('Options').click();
await page.getByText('Delete from history').click();
await page.getByText('• edited').click();
await expect(page.locator(".content-history-menu .item s span[data-history-is-deleted='1']")).toBeVisible();
});
test('Animation spinner', async ({page}) => {
test.skip(issueURL === '', 'previous test failed');
const response = await page.goto(issueURL);
expect(response?.status()).toBe(200);
// Intercept request to get content history list.
let called = false;
page.on('request', async (request) => {
if (!request.url().includes('/content-history/list')) {
return;
}
called = true;
// Assert the dropdown has a animation spinner.
await expect(page.getByText('• edited')).toHaveClass(/is-loading/);
});
// Open the menu.
await page.getByText('• edited').click();
// Wait until the menu is visible.
await expect(page.locator('.content-history-menu .item:nth-child(1)')).toBeVisible();
// Expect that there was a request by fomantic.
expect(called).toBeTruthy();
// Expect that there is no longer a animation spinner.
await expect(page.getByText('• edited')).not.toHaveClass(/is-loading/);
// Expect that there is no animation spinner after clicking inside the dropdown.
await page.click('.content-history-menu .item:nth-child(2)');
await expect(page.getByText('• edited')).not.toHaveClass(/is-loading/);
await page.click('.content-history-detail-dialog .close');
// Open the menu.
await page.getByText('• edited').click();
// Wait until the menu is visible.
await expect(page.locator('.content-history-menu .item:nth-child(1)')).toBeVisible();
// Close the menu.
await page.getByText('• edited').click();
// Expect that there is no animation spinner.
await expect(page.getByText('• edited')).not.toHaveClass(/is-loading/);
});
});