// @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 {accessibilityCheck} from './shared/accessibility.ts';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(''); // 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('Markdown insert link', async ({page}) => { const response = await page.goto('/user2/repo1/issues/new'); expect(response?.status()).toBe(200); const newLinkButton = page.locator('button[data-md-action="new-link"]'); await newLinkButton.click(); const newLinkModal = page.locator('div[data-markdown-link-modal-id="0"]'); await expect(newLinkModal).toBeVisible(); await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"]'], [], []); await save_visual(page); const url = 'https://example.com'; const description = 'Where does this lead?'; await newLinkModal.locator('input[name="link-url"]').fill(url); await newLinkModal.locator('input[name="link-description"]').fill(description); await newLinkModal.locator('button[data-selector-name="ok-button"]').click(); await expect(newLinkModal).toBeHidden(); const textarea = page.locator('textarea[name=content]'); await expect(textarea).toHaveValue(`[${description}](${url})`); 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* `);}); test('Combo Markdown: preview mode switch', async ({page}) => { // Load page with editor const response = await page.goto('/user2/repo1/issues/new'); expect(response?.status()).toBe(200); const toolbarItem = page.locator('md-header'); const editorPanel = page.locator('[data-tab-panel="markdown-writer"]'); const previewPanel = page.locator('[data-tab-panel="markdown-previewer"]'); // Verify correct visibility of related UI elements await expect(toolbarItem).toBeVisible(); await expect(editorPanel).toBeVisible(); await expect(previewPanel).toBeHidden(); // Fill some content const textarea = page.locator('textarea.markdown-text-editor'); await textarea.fill('**Content** :100: _100_'); // Switch to preview mode await page.locator('a[data-tab-for="markdown-previewer"]').click(); // Verify that the related UI elements were switched correctly await expect(toolbarItem).toBeHidden(); await expect(editorPanel).toBeHidden(); await expect(previewPanel).toBeVisible(); await save_visual(page); // Verify that some content rendered await expect(page.locator('[data-tab-panel="markdown-previewer"] .emoji[data-alias="100"]')).toBeVisible(); // Switch back to edit mode await page.locator('a[data-tab-for="markdown-writer"]').click(); // Verify that the related UI elements were switched back correctly await expect(toolbarItem).toBeVisible(); await expect(editorPanel).toBeVisible(); await expect(previewPanel).toBeHidden(); await save_visual(page);});
// @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 {accessibilityCheck} from './shared/accessibility.ts';
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('');
// 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('Markdown insert link', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
const newLinkButton = page.locator('button[data-md-action="new-link"]');
await newLinkButton.click();
const newLinkModal = page.locator('div[data-markdown-link-modal-id="0"]');
await expect(newLinkModal).toBeVisible();
await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"]'], [], []);
await save_visual(page);
const url = 'https://example.com';
const description = 'Where does this lead?';
await newLinkModal.locator('input[name="link-url"]').fill(url);
await newLinkModal.locator('input[name="link-description"]').fill(description);
await newLinkModal.locator('button[data-selector-name="ok-button"]').click();
await expect(newLinkModal).toBeHidden();
const textarea = page.locator('textarea[name=content]');
await expect(textarea).toHaveValue(`[${description}](${url})`);
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* `);
});
test('Combo Markdown: preview mode switch', async ({page}) => {
// Load page with editor
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
const toolbarItem = page.locator('md-header');
const editorPanel = page.locator('[data-tab-panel="markdown-writer"]');
const previewPanel = page.locator('[data-tab-panel="markdown-previewer"]');
// Verify correct visibility of related UI elements
await expect(toolbarItem).toBeVisible();
await expect(editorPanel).toBeVisible();
await expect(previewPanel).toBeHidden();
// Fill some content
const textarea = page.locator('textarea.markdown-text-editor');
await textarea.fill('**Content** :100: _100_');
// Switch to preview mode
await page.locator('a[data-tab-for="markdown-previewer"]').click();
// Verify that the related UI elements were switched correctly
await expect(toolbarItem).toBeHidden();
await expect(editorPanel).toBeHidden();
await expect(previewPanel).toBeVisible();
await save_visual(page);
// Verify that some content rendered
await expect(page.locator('[data-tab-panel="markdown-previewer"] .emoji[data-alias="100"]')).toBeVisible();
// Switch back to edit mode
await page.locator('a[data-tab-for="markdown-writer"]').click();
// Verify that the related UI elements were switched back correctly
await expect(toolbarItem).toBeVisible();
await expect(editorPanel).toBeVisible();
await expect(previewPanel).toBeHidden();
await save_visual(page);
});