mirror of
https://codeberg.org/forgejo/forgejo
synced 2025-10-19 00:40:51 +02:00
When you edit a comment and the comment already has a markdown editor, then the code will click on the 'Write' tab, in case you canceled editting the comment when you were at the 'Preview' tab. In forgejo/forgejo#2681 I added `href="#"` to the tab items, this causes that when the 'Write' tab is being clicked by the code the page is jumped the beginning of the page. Instead of being clever and trying to make this item interactive via another way or via javascript avoid this jumping, we do better and make this element a button. This item is not a link, it's a button that will perform a action. This entirely avoids the issue of jumping and it's still interactive. Resolves forgejo/forgejo#9542 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9645 Reviewed-by: Beowulf <beowulf@beocode.eu> Co-authored-by: Gusted <postmaster@gusted.xyz> Co-committed-by: Gusted <postmaster@gusted.xyz>
322 lines
15 KiB
TypeScript
322 lines
15 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', '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' +
|
|
'> <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: [](/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('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();
|
|
});
|