forgejo/tests/e2e/markdown-editor.test.e2e.ts
Beowulf 28e0af23fa
Some checks are pending
testing-integration / test-sqlite (push) Waiting to run
testing-integration / test-mariadb (v10.6) (push) Waiting to run
testing-integration / test-mariadb (v11.8) (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing / security-check (push) Blocked by required conditions
feat(ui): replace Monaco with CodeMirror (#10559)
- Replace the [Monaco Editor](https://microsoft.github.io/monaco-editor/)
with [CodeMirror 6](https://codemirror.net/). This editor is used to
facilitate the 'Add file' and 'Edit file' functionality.
- Rationale:
  - Monaco editor is a great and powerful editor, however for Forgejo's
  purpose it acts more like a small IDE than a code editor and is doing
  too much. In my limited user research the usage of editing files via
  the web UI is largely for small changes that does not need the
  features that Monaco editor provides.
  - Monaco editor has no mobile support, Codemirror is very usable on mobile.
  - Monaco editor pulls in large dependencies (for language support) and
  by replacing it with Codemirror the amount of time that webpack needs
  to build the frontend is reduced by 50% (~30s -> ~15s).
  - The binary of Forgejo (build with `bindata` tag) is reduced by 2MiB.
  - Codemirror is much more lightweight and should be more usable on
  less powerful hardware, most notably the lazy loading is much faster
  as codemirror uses less javascript.
  - Because Codemirror is modular it is much easier to change the
  behavior of the code editor if we wish to.
- Drawbacks:
  - Codemirror is quite modular and as seen in `package.json` and in
  `codeeditor.ts` we have to supply a lot more of its features to have
  feature parity with Monaco editor.
  - Monaco editor has great integrated language support (features that
  an lsp would provide), Codemirror only has such language support to an
  extend.
  - Monaco editor has its famous command palette (known by many as its
  also available in VSCode), this is not available in code mirror.
- Good to note:
  - All features that was added on top of the monaco editor (such as
  dynamically changing language  support depending on the filename)
  still works and the theme is based on the VSCode colors which largely
  resembles the monaco editor.
  - The code editor is still lazy-loaded (this is painfully clear by
  reading how imports are passed around in `codeeditor.ts`).
  - This change was privately tested by a few people, a few bugs were
  found (and fixed) but no major drawbacks were noted for their usage of
  the web editor.
  - There's a "search" button in the top bar, so that search can be used
  on mobile. It is otherwise only accessible via
  <kbd>Ctrl</kbd>+<kbd>f</kbd>.

Co-authored-by: Beowulf <beowulf@beocode.eu>

Co-authored-by: Gusted <postmaster@gusted.xyz>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10559
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-committed-by: Beowulf <beowulf@beocode.eu>
2026-01-04 23:52:33 +01:00

587 lines
24 KiB
TypeScript

// @watch start
// web_src/js/modules/tab.ts
// web_src/css/modules/tab.css
// 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 {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
test('Markdown image preview behaviour', async ({page}) => {
// 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 codemirror editor
await page.locator('.cm-content').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('button[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 screenshot(page);
});
test('Markdown indentation via toolbar', 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);
// 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 indentation with Tab', 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 toast = page.locator('.toastify');
const tab = ' ';
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 textarea.press('Tab');
await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`);
await textarea.press('Shift+Tab');
await expect(textarea).toHaveValue(initText);
// Attempt unindent again, ensure focus is not immediately lost and toast is shown, but then focus is lost on next attempt.
await expect(toast).toBeHidden(); // toast should not already be there
await textarea.press('Shift+Tab');
await expect(textarea).toBeFocused();
await expect(toast).toBeVisible();
await textarea.press('Shift+Tab');
await expect(textarea).not.toBeFocused();
// Indent lines 2-4
await textarea.click();
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('\n') + 1, it.value.length));
await textarea.press('Tab');
await expect(textarea).toHaveValue(`* first\n${tab}* second\n${tab}* third\n${tab}* last`);
// Indent second line while in whitespace, then unindent.
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf(' * third'), it.value.indexOf(' * third')));
await textarea.press('Tab');
await expect(textarea).toHaveValue(`* first\n${tab}* second\n${tab}${tab}* third\n${tab}* last`);
await textarea.press('Shift+Tab');
await expect(textarea).toHaveValue(`* first\n${tab}* second\n${tab}* third\n${tab}* last`);
// Select all and unindent, then lose focus.
await textarea.evaluate((it:HTMLTextAreaElement) => it.select());
await textarea.press('Shift+Tab'); // Everything is unindented.
await expect(textarea).toHaveValue(initText);
await textarea.press('Shift+Tab'); // Valid, but nothing happens -> switch to "about to lose focus" state.
await expect(textarea).toBeFocused();
await textarea.press('Shift+Tab');
await expect(textarea).not.toBeFocused();
// Attempt the same with cursor within list element body.
await textarea.focus();
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0));
await textarea.press('ArrowRight');
await textarea.press('ArrowRight');
await textarea.press('Tab');
// Whole line should be indented.
await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`);
await textarea.press('Shift+Tab');
// Subsequently, select a chunk of 2nd and 3rd line and indent both, preserving the cursor position in relation to text
const line3 = `* first\n* second\n${tab}* third\n* last`;
const lines23 = `* first\n${tab}* second\n${tab}${tab}* third\n* last`;
await textarea.focus();
await textarea.fill(line3);
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('hird')));
await textarea.press('Tab');
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 textarea.press('Shift+Tab');
await expect(textarea).toHaveValue(line3);
await textarea.press('Shift+Tab');
await expect(textarea).toHaveValue(initText);
// 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 textarea.press('Shift+Tab');
await expect(textarea).toHaveValue(initText);
// Check that indentation tokens not at the start of the string do not interrupt indentation
await textarea.focus();
await textarea.fill(initText);
await textarea.pressSequentially(tab);
await textarea.press('Tab');
await expect(textarea).toHaveValue(`* first\n* second\n* third\n * last `);
});
test('markdown block quote 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 toast = page.locator('.toastify');
await textarea.fill(initText);
await textarea.click(); // Tab handling is disabled until pointer event or input.
// Indent, then unindent first line twice (quotes can quote quotes!)
await textarea.focus();
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0));
await textarea.press('Tab');
await expect(textarea).toHaveValue(`> > first\n> second\n> third\n> last`);
await textarea.press('Tab');
await expect(textarea).toHaveValue(`> > > first\n> second\n> third\n> last`);
await textarea.press('Shift+Tab');
await textarea.press('Shift+Tab');
await expect(textarea).toHaveValue(initText);
// Attempt unindent again.
await expect(toast).toBeHidden(); // toast should not already be there
await textarea.press('Shift+Tab');
// Nothing happens - quote should not stop being a quote
await expect(textarea).toHaveValue(initText);
// Focus is not immediately lost and toast is shown,
await expect(textarea).toBeFocused();
await expect(toast).toBeVisible();
// Focus is lost on next attempt,
await textarea.press('Shift+Tab');
await expect(textarea).not.toBeFocused();
// Indent lines 2-4
await textarea.click();
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('\n') + 1, it.value.length));
await textarea.press('Tab');
await expect(textarea).toHaveValue(`> first\n> > second\n> > third\n> > last`);
// Select all and unindent, then lose focus.
await textarea.evaluate((it:HTMLTextAreaElement) => it.select());
await textarea.press('Shift+Tab'); // Everything is unindented.
await expect(textarea).toHaveValue(initText);
await textarea.press('Shift+Tab'); // Valid, but nothing happens -> switch to "about to lose focus" state.
await expect(textarea).toBeFocused();
await textarea.press('Shift+Tab');
await expect(textarea).not.toBeFocused();
});
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 screenshot(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 screenshot(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 screenshot(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 screenshot(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('[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 screenshot(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('[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();
// Validate switch height: it is customized to be same height as other buttons on the panel
expect(await page.locator('markdown-toolbar .switch').evaluate((el) => getComputedStyle(el).height)).toBe(await page.locator('md-header.markdown-toolbar-button').evaluate((el) => getComputedStyle(el).height));
await screenshot(page);
});
test('Multiple combo markdown: insert table', async ({page}) => {
const response = await page.goto('/user2/multiple-combo-boxes/issues/new?template=.forgejo%2fissue_template%2fmulti-combo-boxes.yaml');
expect(response?.status()).toBe(200);
// check that there are two textareas
const textareaOne = page.locator('textarea[name=form-field-textarea-one]');
const comboboxOne = page.locator('textarea#_combo_markdown_editor_0');
await expect(textareaOne).toBeVisible();
await expect(comboboxOne).toBeHidden();
const textareaTwo = page.locator('textarea[name=form-field-textarea-two]');
const comboboxTwo = page.locator('textarea#_combo_markdown_editor_1');
await expect(textareaTwo).toBeVisible();
await expect(comboboxTwo).toBeHidden();
// focus first one and add table to it
await textareaOne.click();
await expect(comboboxOne).toBeVisible();
await expect(comboboxTwo).toBeHidden();
const newTableButtonOne = page.locator('[for="_combo_markdown_editor_0"] button[data-md-action="new-table"]');
await newTableButtonOne.click();
const newTableModalOne = page.locator('div[data-markdown-table-modal-id="0"]');
await expect(newTableModalOne).toBeVisible();
await newTableModalOne.locator('input[name="table-rows"]').fill('3');
await newTableModalOne.locator('input[name="table-columns"]').fill('2');
await newTableModalOne.locator('button[data-selector-name="ok-button"]').click();
await expect(newTableModalOne).toBeHidden();
await expect(comboboxOne).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
await expect(comboboxTwo).toBeEmpty();
await screenshot(page);
// focus second one and add table to it
await textareaTwo.click();
await expect(comboboxOne).toBeHidden();
await expect(comboboxTwo).toBeVisible();
const newTableButtonTwo = page.locator('[for="_combo_markdown_editor_1"] button[data-md-action="new-table"]');
await newTableButtonTwo.click();
const newTableModalTwo = page.locator('div[data-markdown-table-modal-id="1"]');
await expect(newTableModalTwo).toBeVisible();
await newTableModalTwo.locator('input[name="table-rows"]').fill('2');
await newTableModalTwo.locator('input[name="table-columns"]').fill('3');
await newTableModalTwo.locator('button[data-selector-name="ok-button"]').click();
await expect(newTableModalTwo).toBeHidden();
await expect(comboboxOne).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
await expect(comboboxTwo).toHaveValue('| Header | Header | Header |\n|---------|---------|---------|\n| Content | Content | Content |\n| Content | Content | Content |\n');
await screenshot(page);
});
test('Markdown bold/italic toolbar and shortcut', async ({page}) => {
const initText = `line 1\nline 2\nline 3\nline 4`;
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
const textarea = page.locator('textarea[name=content]');
await textarea.fill(initText);
await textarea.focus();
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('line 1'), it.value.indexOf('line 2')));
// Cases: bold via toolbar, bold via shortcut, repeat w/ italics
page.locator('md-bold').click();
await expect(textarea).toHaveValue(`**line 1**\nline 2\nline 3\nline 4`);
page.locator('md-bold').click();
await expect(textarea).toHaveValue(`line 1\nline 2\nline 3\nline 4`);
await textarea.press('ControlOrMeta+KeyB');
await expect(textarea).toHaveValue(`**line 1**\nline 2\nline 3\nline 4`);
await textarea.press('ControlOrMeta+KeyB');
await expect(textarea).toHaveValue(`line 1\nline 2\nline 3\nline 4`);
page.locator('md-italic').click();
await expect(textarea).toHaveValue(`_line 1_\nline 2\nline 3\nline 4`);
page.locator('md-italic').click();
await expect(textarea).toHaveValue(`line 1\nline 2\nline 3\nline 4`);
await textarea.press('ControlOrMeta+KeyI');
await expect(textarea).toHaveValue(`_line 1_\nline 2\nline 3\nline 4`);
await textarea.press('ControlOrMeta+KeyI');
await expect(textarea).toHaveValue(`line 1\nline 2\nline 3\nline 4`);
});
test('Monospace button aria-label', async ({page}) => {
// Load page with editor
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
const monospaceButton = page.locator('.markdown-switch-monospace');
const enableText = await monospaceButton.getAttribute('data-enable-text');
const disableText = await monospaceButton.getAttribute('data-disable-text');
async function assertAriaLabel(enabled: boolean) {
const expected = enabled ? disableText : enableText;
await expect(monospaceButton).toHaveAttribute('aria-label', expected);
}
const enabled = await monospaceButton.getAttribute('aria-checked') === 'true';
await assertAriaLabel(enabled);
await monospaceButton.click();
await assertAriaLabel(!enabled);
await monospaceButton.click();
await assertAriaLabel(enabled);
});