mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-02-07 18:56:35 -05:00
7ea62c5ce4
When editing a list or similar syntax elements, pressing enter starts a new line with the line introducer (e.g. `- ` for a plain list). But currently it's uncomfortable when someone wants to leave the list. Pressing enter again simply adds more and more lines with the prefix. With this change the list is terminated if enter is pressed on a line which contains the introducer but nothing else. This behavior is known from other markdown editors like the on used by GitLab or GitHub. Additionally I changed the regex for detecting a prefix. - Why: With the change you can add a single whitespace at the end if you want to keep an "empty" line. So if you want to write: ``` - First - - Third ``` You just need to add a whitespace in the second line to prevent that the prefix will be removed. - Changes in detail: - ordered bullet list prefix detection: nothing changed - todo list and unordered list prefix detection: have been split up: - todo list: Changed that only 1 to 4 whitespaces can be between the list char (`-`,`*`,`+`) and the checkbox (`[ ]`,`[x]`) - Why? If more then 4 spaces are between the list char and the checkbox, this is no longer detected as a prefix for a todo item based on the markdown standard. Due to the amount of spaces it is instead parsed as code. - unordered list: The prefix now needs to have exactly one space after the list char (`-`,`*`,`+`). More spaces will not be taken into account for detecting the prefix. - quote prefix detection: nothing changed The current e2e-tests where simplified and duplicated tests where removed. Test cases for the new functionality where added.
251 lines
10 KiB
TypeScript
251 lines
10 KiB
TypeScript
// @watch start
|
|
// web_src/js/features/comp/ComboMarkdownEditor.js
|
|
// web_src/css/editor/combomarkdowneditor.css
|
|
// templates/shared/combomarkdowneditor.tmpl
|
|
// @watch end
|
|
|
|
import {expect} from '@playwright/test';
|
|
import {save_visual, test} from './utils_e2e.ts';
|
|
|
|
test.use({user: 'user2'});
|
|
|
|
test('Markdown image preview behaviour', async ({page}, workerInfo) => {
|
|
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari;');
|
|
|
|
// Editing the root README.md file for image preview
|
|
const editPath = '/user2/repo1/src/branch/master/README.md';
|
|
|
|
const response = await page.goto(editPath, {waitUntil: 'domcontentloaded'});
|
|
expect(response?.status()).toBe(200);
|
|
|
|
// Click 'Edit file' tab
|
|
await page.locator('[data-tooltip-content="Edit file"]').click();
|
|
|
|
// This yields the monaco editor
|
|
const editor = page.getByRole('presentation').nth(0);
|
|
await editor.click();
|
|
// Clear all the content
|
|
await page.keyboard.press('ControlOrMeta+KeyA');
|
|
// Add the image
|
|
await page.keyboard.type('![Logo of Forgejo](./assets/logo.svg "Logo of Forgejo")');
|
|
|
|
// Click 'Preview' tab
|
|
await page.locator('a[data-tab="preview"]').click();
|
|
|
|
// 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);
|
|
});
|
|
|
|
test('markdown indentation', async ({page}) => {
|
|
const initText = `* first\n* second\n* third\n* last`;
|
|
|
|
const response = await page.goto('/user2/repo1/issues/new');
|
|
expect(response?.status()).toBe(200);
|
|
|
|
const textarea = page.locator('textarea[name=content]');
|
|
const tab = ' ';
|
|
const indent = page.locator('button[data-md-action="indent"]');
|
|
const unindent = page.locator('button[data-md-action="unindent"]');
|
|
await textarea.fill(initText);
|
|
await textarea.click(); // Tab handling is disabled until pointer event or input.
|
|
|
|
// Indent, then unindent first line
|
|
await textarea.focus();
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0));
|
|
await indent.click();
|
|
await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`);
|
|
await unindent.click();
|
|
await expect(textarea).toHaveValue(initText);
|
|
|
|
// Indent second line while somewhere inside of it
|
|
await textarea.focus();
|
|
await textarea.press('ArrowDown');
|
|
await textarea.press('ArrowRight');
|
|
await textarea.press('ArrowRight');
|
|
await indent.click();
|
|
await expect(textarea).toHaveValue(`* first\n${tab}* second\n* third\n* last`);
|
|
|
|
// Subsequently, select a chunk of 2nd and 3rd line and indent both, preserving the cursor position in relation to text
|
|
await textarea.focus();
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('hird')));
|
|
await indent.click();
|
|
const lines23 = `* first\n${tab}${tab}* second\n${tab}* third\n* last`;
|
|
await expect(textarea).toHaveValue(lines23);
|
|
await expect(textarea).toHaveJSProperty('selectionStart', lines23.indexOf('cond'));
|
|
await expect(textarea).toHaveJSProperty('selectionEnd', lines23.indexOf('hird'));
|
|
|
|
// Then unindent twice, erasing all indents.
|
|
await unindent.click();
|
|
await expect(textarea).toHaveValue(`* first\n${tab}* second\n* third\n* last`);
|
|
await unindent.click();
|
|
await expect(textarea).toHaveValue(initText);
|
|
|
|
// Indent and unindent with cursor at the end of the line
|
|
await textarea.focus();
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond')));
|
|
await textarea.press('End');
|
|
await indent.click();
|
|
await expect(textarea).toHaveValue(`* first\n${tab}* second\n* third\n* last`);
|
|
await unindent.click();
|
|
await expect(textarea).toHaveValue(initText);
|
|
|
|
// Check that Tab does work after input
|
|
await textarea.focus();
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
|
await textarea.press('Shift+Enter'); // Avoid triggering the prefix continuation feature
|
|
await textarea.pressSequentially('* least');
|
|
await indent.click();
|
|
await expect(textarea).toHaveValue(`* first\n* second\n* third\n* last\n${tab}* least`);
|
|
|
|
// Check that partial indents are cleared
|
|
await textarea.focus();
|
|
await textarea.fill(initText);
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('* second'), it.value.indexOf('* second')));
|
|
await textarea.pressSequentially(' ');
|
|
await unindent.click();
|
|
await expect(textarea).toHaveValue(initText);
|
|
});
|
|
|
|
test('markdown list continuation', async ({page}) => {
|
|
const initText = `* first\n* second`;
|
|
|
|
const response = await page.goto('/user2/repo1/issues/new');
|
|
expect(response?.status()).toBe(200);
|
|
|
|
const textarea = page.locator('textarea[name=content]');
|
|
const tab = ' ';
|
|
const indent = page.locator('button[data-md-action="indent"]');
|
|
await textarea.fill(initText);
|
|
|
|
// Test continuation of ' * ' prefix
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('rst'), it.value.indexOf('rst')));
|
|
await indent.click();
|
|
await textarea.press('End');
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially('muddle');
|
|
await expect(textarea).toHaveValue(`${tab}* first\n${tab}* muddle\n* second`);
|
|
|
|
// Test breaking in the middle of a line
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.lastIndexOf('ddle'), it.value.lastIndexOf('ddle')));
|
|
await textarea.pressSequentially('tate');
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially('me');
|
|
await expect(textarea).toHaveValue(`${tab}* first\n${tab}* mutate\n${tab}* meddle\n* second`);
|
|
|
|
// Test not triggering when Shift held
|
|
await textarea.fill(initText);
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
|
await textarea.press('Shift+Enter');
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially('...but not least');
|
|
await expect(textarea).toHaveValue(`* first\n* second\n\n...but not least`);
|
|
|
|
// Test continuation of ordered list
|
|
await textarea.fill(`1. one`);
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially(' ');
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially('three');
|
|
await textarea.press('Enter');
|
|
await textarea.press('Enter');
|
|
await expect(textarea).toHaveValue(`1. one\n2. \n3. three\n\n`);
|
|
|
|
// Test continuation of alternative ordered list syntax
|
|
await textarea.fill(`1) one`);
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially(' ');
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially('three');
|
|
await textarea.press('Enter');
|
|
await textarea.press('Enter');
|
|
await expect(textarea).toHaveValue(`1) one\n2) \n3) three\n\n`);
|
|
|
|
// Test continuation of checklists
|
|
await textarea.fill(`- [ ]have a problem\n- [x]create a solution`);
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially('write a test');
|
|
await expect(textarea).toHaveValue(`- [ ]have a problem\n- [x]create a solution\n- [ ]write a test`);
|
|
|
|
// Test all conceivable syntax (except ordered lists)
|
|
const prefixes = [
|
|
'- ', // A space between the bullet and the content is required.
|
|
' - ', // I have seen single space in front of -/* being used and even recommended, I think.
|
|
'* ',
|
|
'+ ',
|
|
' ',
|
|
' ',
|
|
' - ',
|
|
'\t',
|
|
'\t\t* ',
|
|
'> ',
|
|
'> > ',
|
|
'- [ ] ',
|
|
'* [ ] ',
|
|
'+ [ ] ',
|
|
];
|
|
for (const prefix of prefixes) {
|
|
await textarea.fill(`${prefix}one`);
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially(' ');
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially('two');
|
|
await textarea.press('Enter');
|
|
await textarea.press('Enter');
|
|
await expect(textarea).toHaveValue(`${prefix}one\n${prefix} \n${prefix}two\n\n`);
|
|
}
|
|
});
|
|
|
|
test('markdown insert table', async ({page}) => {
|
|
const response = await page.goto('/user2/repo1/issues/new');
|
|
expect(response?.status()).toBe(200);
|
|
|
|
const newTableButton = page.locator('button[data-md-action="new-table"]');
|
|
await newTableButton.click();
|
|
|
|
const newTableModal = page.locator('div[data-markdown-table-modal-id="0"]');
|
|
await expect(newTableModal).toBeVisible();
|
|
await save_visual(page);
|
|
|
|
await newTableModal.locator('input[name="table-rows"]').fill('3');
|
|
await newTableModal.locator('input[name="table-columns"]').fill('2');
|
|
|
|
await newTableModal.locator('button[data-selector-name="ok-button"]').click();
|
|
|
|
await expect(newTableModal).toBeHidden();
|
|
|
|
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);
|
|
});
|
|
|
|
test('text expander has higher prio then prefix continuation', async ({page}) => {
|
|
const response = await page.goto('/user2/repo1/issues/new');
|
|
expect(response?.status()).toBe(200);
|
|
|
|
const textarea = page.locator('textarea[name=content]');
|
|
const initText = `* first`;
|
|
await textarea.fill(initText);
|
|
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('rst'), it.value.indexOf('rst')));
|
|
await textarea.press('End');
|
|
|
|
// Test emoji completion
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially(':smile_c');
|
|
await textarea.press('Enter');
|
|
await expect(textarea).toHaveValue(`* first\n* 😸`);
|
|
|
|
// Test username completion
|
|
await textarea.press('Enter');
|
|
await textarea.pressSequentially('@user');
|
|
await textarea.press('Enter');
|
|
await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 `);
|
|
|
|
await textarea.press('Enter');
|
|
await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 \n* `);
|
|
});
|