// @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', 'Mobile Safari'].includes(workerInfo.project.name), 'Mobile Chrome has trouble clicking Comment button with JS enabled, Mobile Safari is flaky and only passes on retry'); 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('Hyperlink paste behaviour', async ({page}, workerInfo) => { test.skip(['Mobile Safari', 'Mobile Chrome', 'webkit'].includes(workerInfo.project.name), 'Mobile clients seem to have very weird behaviour with this test, which I cannot confirm with real usage'); 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' + '> something something\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' + '> something something\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('Comment history', 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+$/); 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(); });