1// @watch start
2// web_src/js/features/comp/ComboMarkdownEditor.js
3// web_src/css/editor/combomarkdowneditor.css
4// templates/shared/combomarkdowneditor.tmpl
5// @watch end
6
7import {expect} from '@playwright/test';
8import {accessibilityCheck} from './shared/accessibility.ts';
9import {save_visual, test} from './utils_e2e.ts';
10
11test.use({user: 'user2'});
12
13test('Markdown image preview behaviour', async ({page}, workerInfo) => {
14 test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari;');
15
16 // Editing the root README.md file for image preview
17 const editPath = '/user2/repo1/src/branch/master/README.md';
18
19 const response = await page.goto(editPath, {waitUntil: 'domcontentloaded'});
20 expect(response?.status()).toBe(200);
21
22 // Click 'Edit file' tab
23 await page.locator('[data-tooltip-content="Edit file"]').click();
24
25 // This yields the monaco editor
26 const editor = page.getByRole('presentation').nth(0);
27 await editor.click();
28 // Clear all the content
29 await page.keyboard.press('ControlOrMeta+KeyA');
30 // Add the image
31 await page.keyboard.type('');
32
33 // Click 'Preview' tab
34 await page.locator('a[data-tab="preview"]').click();
35
36 // Check for the image preview via the expected attribute
37 const preview = page.locator('div[data-tab="preview"] p[dir="auto"] a');
38 await expect(preview).toHaveAttribute('href', 'http://localhost:3003/user2/repo1/media/branch/master/assets/logo.svg');
39 await save_visual(page);
40});
41
42test('Markdown indentation', async ({page}) => {
43 const initText = `* first\n* second\n* third\n* last`;
44
45 const response = await page.goto('/user2/repo1/issues/new');
46 expect(response?.status()).toBe(200);
47
48 const textarea = page.locator('textarea[name=content]');
49 const tab = ' ';
50 const indent = page.locator('button[data-md-action="indent"]');
51 const unindent = page.locator('button[data-md-action="unindent"]');
52 await textarea.fill(initText);
53 await textarea.click(); // Tab handling is disabled until pointer event or input.
54
55 // Indent, then unindent first line
56 await textarea.focus();
57 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0));
58 await indent.click();
59 await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`);
60 await unindent.click();
61 await expect(textarea).toHaveValue(initText);
62
63 // Indent second line while somewhere inside of it
64 await textarea.focus();
65 await textarea.press('ArrowDown');
66 await textarea.press('ArrowRight');
67 await textarea.press('ArrowRight');
68 await indent.click();
69 await expect(textarea).toHaveValue(`* first\n${tab}* second\n* third\n* last`);
70
71 // Subsequently, select a chunk of 2nd and 3rd line and indent both, preserving the cursor position in relation to text
72 await textarea.focus();
73 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('hird')));
74 await indent.click();
75 const lines23 = `* first\n${tab}${tab}* second\n${tab}* third\n* last`;
76 await expect(textarea).toHaveValue(lines23);
77 await expect(textarea).toHaveJSProperty('selectionStart', lines23.indexOf('cond'));
78 await expect(textarea).toHaveJSProperty('selectionEnd', lines23.indexOf('hird'));
79
80 // Then unindent twice, erasing all indents.
81 await unindent.click();
82 await expect(textarea).toHaveValue(`* first\n${tab}* second\n* third\n* last`);
83 await unindent.click();
84 await expect(textarea).toHaveValue(initText);
85
86 // Indent and unindent with cursor at the end of the line
87 await textarea.focus();
88 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond')));
89 await textarea.press('End');
90 await indent.click();
91 await expect(textarea).toHaveValue(`* first\n${tab}* second\n* third\n* last`);
92 await unindent.click();
93 await expect(textarea).toHaveValue(initText);
94
95 // Check that Tab does work after input
96 await textarea.focus();
97 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
98 await textarea.press('Shift+Enter'); // Avoid triggering the prefix continuation feature
99 await textarea.pressSequentially('* least');
100 await indent.click();
101 await expect(textarea).toHaveValue(`* first\n* second\n* third\n* last\n${tab}* least`);
102
103 // Check that partial indents are cleared
104 await textarea.focus();
105 await textarea.fill(initText);
106 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('* second'), it.value.indexOf('* second')));
107 await textarea.pressSequentially(' ');
108 await unindent.click();
109 await expect(textarea).toHaveValue(initText);
110});
111
112test('Markdown list continuation', async ({page}) => {
113 const initText = `* first\n* second`;
114
115 const response = await page.goto('/user2/repo1/issues/new');
116 expect(response?.status()).toBe(200);
117
118 const textarea = page.locator('textarea[name=content]');
119 const tab = ' ';
120 const indent = page.locator('button[data-md-action="indent"]');
121 await textarea.fill(initText);
122
123 // Test continuation of ' * ' prefix
124 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('rst'), it.value.indexOf('rst')));
125 await indent.click();
126 await textarea.press('End');
127 await textarea.press('Enter');
128 await textarea.pressSequentially('muddle');
129 await expect(textarea).toHaveValue(`${tab}* first\n${tab}* muddle\n* second`);
130
131 // Test breaking in the middle of a line
132 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.lastIndexOf('ddle'), it.value.lastIndexOf('ddle')));
133 await textarea.pressSequentially('tate');
134 await textarea.press('Enter');
135 await textarea.pressSequentially('me');
136 await expect(textarea).toHaveValue(`${tab}* first\n${tab}* mutate\n${tab}* meddle\n* second`);
137
138 // Test not triggering when Shift held
139 await textarea.fill(initText);
140 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
141 await textarea.press('Shift+Enter');
142 await textarea.press('Enter');
143 await textarea.pressSequentially('...but not least');
144 await expect(textarea).toHaveValue(`* first\n* second\n\n...but not least`);
145
146 // Test continuation of ordered list
147 await textarea.fill(`1. one`);
148 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
149 await textarea.press('Enter');
150 await textarea.pressSequentially(' ');
151 await textarea.press('Enter');
152 await textarea.pressSequentially('three');
153 await textarea.press('Enter');
154 await textarea.press('Enter');
155 await expect(textarea).toHaveValue(`1. one\n2. \n3. three\n\n`);
156
157 // Test continuation of alternative ordered list syntax
158 await textarea.fill(`1) one`);
159 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
160 await textarea.press('Enter');
161 await textarea.pressSequentially(' ');
162 await textarea.press('Enter');
163 await textarea.pressSequentially('three');
164 await textarea.press('Enter');
165 await textarea.press('Enter');
166 await expect(textarea).toHaveValue(`1) one\n2) \n3) three\n\n`);
167
168 // Test continuation of checklists
169 await textarea.fill(`- [ ]have a problem\n- [x]create a solution`);
170 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
171 await textarea.press('Enter');
172 await textarea.pressSequentially('write a test');
173 await expect(textarea).toHaveValue(`- [ ]have a problem\n- [x]create a solution\n- [ ]write a test`);
174
175 // Test all conceivable syntax (except ordered lists)
176 const prefixes = [
177 '- ', // A space between the bullet and the content is required.
178 ' - ', // I have seen single space in front of -/* being used and even recommended, I think.
179 '* ',
180 '+ ',
181 ' ',
182 ' ',
183 ' - ',
184 '\t',
185 '\t\t* ',
186 '> ',
187 '> > ',
188 '- [ ] ',
189 '* [ ] ',
190 '+ [ ] ',
191 ];
192 for (const prefix of prefixes) {
193 await textarea.fill(`${prefix}one`);
194 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
195 await textarea.press('Enter');
196 await textarea.pressSequentially(' ');
197 await textarea.press('Enter');
198 await textarea.pressSequentially('two');
199 await textarea.press('Enter');
200 await textarea.press('Enter');
201 await expect(textarea).toHaveValue(`${prefix}one\n${prefix} \n${prefix}two\n\n`);
202 }
203});
204
205test('Markdown insert table', async ({page}) => {
206 const response = await page.goto('/user2/repo1/issues/new');
207 expect(response?.status()).toBe(200);
208
209 const newTableButton = page.locator('button[data-md-action="new-table"]');
210 await newTableButton.click();
211
212 const newTableModal = page.locator('div[data-markdown-table-modal-id="0"]');
213 await expect(newTableModal).toBeVisible();
214 await save_visual(page);
215
216 await newTableModal.locator('input[name="table-rows"]').fill('3');
217 await newTableModal.locator('input[name="table-columns"]').fill('2');
218
219 await newTableModal.locator('button[data-selector-name="ok-button"]').click();
220
221 await expect(newTableModal).toBeHidden();
222
223 const textarea = page.locator('textarea[name=content]');
224 await expect(textarea).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
225 await save_visual(page);
226});
227
228test('Markdown insert link', async ({page}) => {
229 const response = await page.goto('/user2/repo1/issues/new');
230 expect(response?.status()).toBe(200);
231
232 const newLinkButton = page.locator('button[data-md-action="new-link"]');
233 await newLinkButton.click();
234
235 const newLinkModal = page.locator('div[data-markdown-link-modal-id="0"]');
236 await expect(newLinkModal).toBeVisible();
237 await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"]'], [], []);
238 await save_visual(page);
239
240 const url = 'https://example.com';
241 const description = 'Where does this lead?';
242
243 await newLinkModal.locator('input[name="link-url"]').fill(url);
244 await newLinkModal.locator('input[name="link-description"]').fill(description);
245
246 await newLinkModal.locator('button[data-selector-name="ok-button"]').click();
247
248 await expect(newLinkModal).toBeHidden();
249
250 const textarea = page.locator('textarea[name=content]');
251 await expect(textarea).toHaveValue(`[${description}](${url})`);
252 await save_visual(page);
253});
254
255test('text expander has higher prio then prefix continuation', async ({page}) => {
256 const response = await page.goto('/user2/repo1/issues/new');
257 expect(response?.status()).toBe(200);
258
259 const textarea = page.locator('textarea[name=content]');
260 const initText = `* first`;
261 await textarea.fill(initText);
262 await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('rst'), it.value.indexOf('rst')));
263 await textarea.press('End');
264
265 // Test emoji completion
266 await textarea.press('Enter');
267 await textarea.pressSequentially(':smile_c');
268 await textarea.press('Enter');
269 await expect(textarea).toHaveValue(`* first\n* 😸`);
270
271 // Test username completion
272 await textarea.press('Enter');
273 await textarea.pressSequentially('@user');
274 await textarea.press('Enter');
275 await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 `);
276
277 await textarea.press('Enter');
278 await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 \n* `);
279});
280
281test('Combo Markdown: preview mode switch', async ({page}) => {
282 // Load page with editor
283 const response = await page.goto('/user2/repo1/issues/new');
284 expect(response?.status()).toBe(200);
285
286 const toolbarItem = page.locator('md-header');
287 const editorPanel = page.locator('[data-tab-panel="markdown-writer"]');
288 const previewPanel = page.locator('[data-tab-panel="markdown-previewer"]');
289
290 // Verify correct visibility of related UI elements
291 await expect(toolbarItem).toBeVisible();
292 await expect(editorPanel).toBeVisible();
293 await expect(previewPanel).toBeHidden();
294
295 // Fill some content
296 const textarea = page.locator('textarea.markdown-text-editor');
297 await textarea.fill('**Content** :100: _100_');
298
299 // Switch to preview mode
300 await page.locator('a[data-tab-for="markdown-previewer"]').click();
301
302 // Verify that the related UI elements were switched correctly
303 await expect(toolbarItem).toBeHidden();
304 await expect(editorPanel).toBeHidden();
305 await expect(previewPanel).toBeVisible();
306 await save_visual(page);
307
308 // Verify that some content rendered
309 await expect(page.locator('[data-tab-panel="markdown-previewer"] .emoji[data-alias="100"]')).toBeVisible();
310
311 // Switch back to edit mode
312 await page.locator('a[data-tab-for="markdown-writer"]').click();
313
314 // Verify that the related UI elements were switched back correctly
315 await expect(toolbarItem).toBeVisible();
316 await expect(editorPanel).toBeVisible();
317 await expect(previewPanel).toBeHidden();
318 await save_visual(page);
319});