// @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: [](/user2/repo1/attachments/3f4f4016-877b-46b3-b79f-ad24519a9cf2)\n' +
'>
\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: [](/user2/commitsonpr/attachments/3f4f4016-877b-46b3-b79f-ad24519a9cf2)\n' +
'>
\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();
});