Skip to content

Commit 9c23e12

Browse files
BartoszGrajdekSkalakidjmusial
authored
Implement code and pre blocks support on web (Expensify#456)
Co-authored-by: Michał Skałka <[email protected]> Co-authored-by: Jan Musiał <[email protected]>
1 parent f728e4a commit 9c23e12

20 files changed

+599
-88
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ module.exports = {
5050
'es/no-nullish-coalescing-operators': 'off',
5151
'es/no-optional-chaining': 'off',
5252
'@typescript-eslint/no-use-before-define': 'off', // TODO consider enabling this (currently it reports styles defined at the bottom of the file)
53-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
53+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
5454
'@typescript-eslint/consistent-type-imports': [
5555
'error',
5656
{ prefer: 'type-imports' },
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import {test, expect} from '@playwright/test';
2+
import type {Locator, Page} from '@playwright/test';
3+
// eslint-disable-next-line import/no-relative-packages
4+
import * as TEST_CONST from '../../example/src/testConstants';
5+
import {getElementValue, pressCmd, setCursorPosition, setupInput, testMarkdownContentStyle} from './utils';
6+
7+
const CODEBLOCK_DEFAULT_STYLE =
8+
'font-family: monospace; font-size: 20px; color: black; background-color: lightgray; border-color: gray; border-width: 1px; border-radius: 4px; border-style: solid; padding: 2px;';
9+
10+
async function testCodeblockStyle(page: Page, dimmensions: {height: number; width: number} | null, style: string | null = CODEBLOCK_DEFAULT_STYLE) {
11+
if (style === null) {
12+
await testMarkdownContentStyle({
13+
testContent: 'Codeblock',
14+
style: 'margin: 0px; padding: 0px;',
15+
page,
16+
});
17+
return;
18+
}
19+
await testMarkdownContentStyle({
20+
testContent: 'Codeblock',
21+
style,
22+
dimmensions: dimmensions ?? undefined,
23+
page,
24+
});
25+
}
26+
27+
async function getCodeblockElementCount(inputLocator: Locator) {
28+
return inputLocator.locator(`span[data-type="codeblock"]`).count();
29+
}
30+
31+
test.beforeEach(async ({page}) => {
32+
await page.goto(TEST_CONST.LOCAL_URL, {waitUntil: 'load'});
33+
});
34+
35+
test.describe('modifying codeblock content', () => {
36+
test('keep newlines when writing after opening syntax', async ({page}) => {
37+
const inputLocator = await setupInput(page, 'clear');
38+
await inputLocator.focus();
39+
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```');
40+
41+
await setCursorPosition(page, 0);
42+
await inputLocator.pressSequentially('test');
43+
44+
expect(await getElementValue(inputLocator)).toEqual('```test\nCodeblock\nSample code line\n```');
45+
// Verify if the codeblock style wasn't applied
46+
await testCodeblockStyle(page, null, null);
47+
});
48+
49+
test('keep codeblock structure when writing in the empty last line', async ({page}) => {
50+
const inputLocator = await setupInput(page, 'clear');
51+
await inputLocator.focus();
52+
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n\n```');
53+
54+
await setCursorPosition(page, 6, 0);
55+
await inputLocator.pressSequentially('test');
56+
57+
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\ntest\n```');
58+
await testCodeblockStyle(page, {
59+
height: 84,
60+
width: 198,
61+
});
62+
});
63+
64+
test('allow writing after closing syntax', async ({page}) => {
65+
const codeblockDimmensions = {
66+
height: 58,
67+
width: 198,
68+
};
69+
const inputLocator = await setupInput(page, 'clear');
70+
await inputLocator.focus();
71+
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```');
72+
73+
await setCursorPosition(page, 6);
74+
await testCodeblockStyle(page, codeblockDimmensions);
75+
await inputLocator.pressSequentially('test');
76+
77+
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\n```test');
78+
// Verify if when typing after codeblock closing syntax, its height is not changed
79+
await testCodeblockStyle(page, codeblockDimmensions);
80+
});
81+
82+
test('remove whole codeblock', async ({page}) => {
83+
const inputLocator = await setupInput(page, 'clear');
84+
await inputLocator.focus();
85+
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```');
86+
87+
await pressCmd({inputLocator, command: 'a'});
88+
await inputLocator.press('Backspace');
89+
90+
expect(await getElementValue(inputLocator)).toEqual('');
91+
});
92+
93+
test('wrap content', async ({page}) => {
94+
const LINE_TO_ADD = ' very long line of code that should be wrapped';
95+
const inputLocator = await setupInput(page, 'clear');
96+
await inputLocator.focus();
97+
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```');
98+
99+
await setCursorPosition(page, 3);
100+
await inputLocator.pressSequentially(LINE_TO_ADD);
101+
102+
expect(await getElementValue(inputLocator)).toEqual(`\`\`\`\nCodeblock${LINE_TO_ADD}\nSample code line\n\`\`\``);
103+
await testCodeblockStyle(page, {
104+
height: 110,
105+
width: 288,
106+
});
107+
});
108+
109+
test('remove newline after opening syntax', async ({page}) => {
110+
const inputLocator = await setupInput(page, 'clear');
111+
await inputLocator.focus();
112+
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```');
113+
114+
await setCursorPosition(page, 2, 0);
115+
await inputLocator.press('Backspace');
116+
117+
expect(await getElementValue(inputLocator)).toEqual('```Codeblock\nSample code line\n```');
118+
// Verify if the codeblock style wasn't applied
119+
await testCodeblockStyle(page, null, null);
120+
});
121+
122+
test('remove newline after opening syntax with single line content', async ({page}) => {
123+
const inputLocator = await setupInput(page, 'clear');
124+
await inputLocator.focus();
125+
await inputLocator.pressSequentially('```\nCodeblock\n```');
126+
127+
await setCursorPosition(page, 2, 0);
128+
await inputLocator.press('Backspace');
129+
130+
expect(await getElementValue(inputLocator)).toEqual('```Codeblock\n```');
131+
// Verify if the codeblock style wasn't applied
132+
await testCodeblockStyle(page, null, null);
133+
});
134+
135+
test('remove newline before closing syntax', async ({page}) => {
136+
const inputLocator = await setupInput(page, 'clear');
137+
await inputLocator.focus();
138+
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```');
139+
140+
await setCursorPosition(page, 6, 0);
141+
await inputLocator.press('Backspace');
142+
143+
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line```');
144+
// Verify if the codeblock style wasn't applied
145+
await testCodeblockStyle(page, null, null);
146+
});
147+
148+
test('remove newline before closing syntax with one empty line at the end', async ({page}) => {
149+
const inputLocator = await setupInput(page, 'clear');
150+
await inputLocator.focus();
151+
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n\n```');
152+
153+
await setCursorPosition(page, 6, 0);
154+
await inputLocator.press('Backspace');
155+
156+
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\n```');
157+
await testCodeblockStyle(page, {
158+
height: 58,
159+
width: 198,
160+
});
161+
});
162+
163+
test('remove newline before closing syntax with two empy lines at the end', async ({page}) => {
164+
const inputLocator = await setupInput(page, 'clear');
165+
await inputLocator.focus();
166+
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n\n\n```');
167+
168+
await setCursorPosition(page, 6, 0);
169+
await inputLocator.press('Backspace');
170+
171+
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\n\n```');
172+
await testCodeblockStyle(page, {
173+
height: 84,
174+
width: 198,
175+
});
176+
});
177+
178+
test('remove newline before opening syntax', async ({page}) => {
179+
const inputLocator = await setupInput(page, 'clear');
180+
await inputLocator.focus();
181+
await inputLocator.pressSequentially('\n\n```\nCodeblock\nSample code line\n```');
182+
183+
await setCursorPosition(page, 2, 0);
184+
await inputLocator.press('Backspace');
185+
186+
expect(await getElementValue(inputLocator)).toEqual('\n```\nCodeblock\nSample code line\n```');
187+
await testCodeblockStyle(page, {
188+
height: 58,
189+
width: 198,
190+
});
191+
});
192+
193+
test('remove newline between two codeblocks', async ({page}) => {
194+
const inputLocator = await setupInput(page, 'clear');
195+
await inputLocator.focus();
196+
await inputLocator.pressSequentially('```\nCodeblock\nSample code line\n```\n```\nCodeblock\nSecond sample code line\n```');
197+
198+
await setCursorPosition(page, 7, 0);
199+
await inputLocator.press('Backspace');
200+
201+
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\n``````\nCodeblock\nSecond sample code line\n```');
202+
expect(await getCodeblockElementCount(inputLocator)).toEqual(1);
203+
204+
await inputLocator.press('Enter');
205+
206+
expect(await getElementValue(inputLocator)).toEqual('```\nCodeblock\nSample code line\n```\n```\nCodeblock\nSecond sample code line\n```');
207+
expect(await getCodeblockElementCount(inputLocator)).toEqual(2);
208+
});
209+
});
210+
211+
test('update codeblock dimensions when resizing the input', async ({page}) => {
212+
await page.setViewportSize({width: 1280, height: 720});
213+
const inputLocator = await setupInput(page, 'clear');
214+
await inputLocator.focus();
215+
await inputLocator.pressSequentially('```\nCodeblock\nSample very long line of code that should be wrapped\n```');
216+
217+
await testCodeblockStyle(page, {
218+
height: 110,
219+
width: 288,
220+
});
221+
222+
await inputLocator.evaluate((inputElement: HTMLInputElement) => {
223+
const element = inputElement;
224+
element.style.width = '500px';
225+
element.style.height = '200px';
226+
});
227+
await page.waitForTimeout(10);
228+
229+
await testCodeblockStyle(page, {
230+
height: 84,
231+
width: 488,
232+
});
233+
});
234+
235+
test.describe('scrolling into view', () => {
236+
test('scroll to an empty codeblock line', async ({page}) => {
237+
const inputLocator = await setupInput(page, 'clear');
238+
await inputLocator.focus();
239+
await inputLocator.evaluate((inputElement: HTMLInputElement) => {
240+
const element = inputElement;
241+
element.style.height = '100px';
242+
});
243+
await inputLocator.pressSequentially('```\nCodeblock start\n\n\n\n\n\n\n\n\nCodeblock end\n```');
244+
245+
await setCursorPosition(page, 4);
246+
await inputLocator.blur();
247+
await inputLocator.evaluate((inputElement: HTMLInputElement) => {
248+
const element = inputElement;
249+
element.scrollTop = element.scrollHeight;
250+
return element.scrollHeight;
251+
});
252+
253+
await inputLocator.focus();
254+
const scrollTop = await inputLocator.evaluate((inputElement: HTMLInputElement) => {
255+
const element = inputElement;
256+
return element.scrollTop;
257+
});
258+
259+
expect(scrollTop).toBeLessThanOrEqual(30);
260+
});
261+
262+
test('scroll to the cursor after opening syntax', async ({page}) => {
263+
const inputLocator = await setupInput(page, 'clear');
264+
await inputLocator.focus();
265+
await inputLocator.evaluate((inputElement: HTMLInputElement) => {
266+
const element = inputElement;
267+
element.style.height = '100px';
268+
});
269+
await inputLocator.pressSequentially('```\nCodeblock start\n\n\n\n\n\n\n\n\nCodeblock end\n```');
270+
271+
await setCursorPosition(page, 1);
272+
await inputLocator.blur();
273+
await inputLocator.evaluate((inputElement: HTMLInputElement) => {
274+
const element = inputElement;
275+
element.scrollTop = element.scrollHeight;
276+
return element.scrollHeight;
277+
});
278+
279+
await inputLocator.focus();
280+
const scrollTop = await inputLocator.evaluate((inputElement: HTMLInputElement) => {
281+
const element = inputElement;
282+
return element.scrollTop;
283+
});
284+
285+
expect(scrollTop).toBeLessThanOrEqual(25);
286+
});
287+
});

WebExample/__tests__/styles.spec.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
1-
import {test, expect} from '@playwright/test';
2-
import type {Page} from '@playwright/test';
1+
import {test} from '@playwright/test';
32
// eslint-disable-next-line import/no-relative-packages
43
import * as TEST_CONST from '../../example/src/testConstants';
5-
import {setupInput, getElementStyle} from './utils';
6-
7-
const testMarkdownContentStyle = async ({testContent, style, page}: {testContent: string; style: string; page: Page}) => {
8-
const inputLocator = await setupInput(page);
9-
10-
const elementHandle = inputLocator.locator('span', {hasText: testContent}).last();
11-
const elementStyle = await getElementStyle(elementHandle);
12-
13-
expect(elementStyle).toEqual(style);
14-
};
4+
import {testMarkdownContentStyle} from './utils';
155

166
test.beforeEach(async ({page}) => {
177
await page.goto(TEST_CONST.LOCAL_URL, {waitUntil: 'load'});
@@ -32,11 +22,21 @@ test.describe('markdown content styling', () => {
3222
});
3323

3424
test('inline code', async ({page}) => {
35-
await testMarkdownContentStyle({testContent: 'inline code', style: 'font-family: monospace; font-size: 20px; color: black; background-color: lightgray;', page});
25+
await testMarkdownContentStyle({
26+
testContent: 'inline code',
27+
style:
28+
'font-family: monospace; font-size: 20px; color: black; background-color: lightgray; border-color: gray; border-width: 1px; border-radius: 4px; border-style: solid; padding: 0px; line-height: 1.5;',
29+
page,
30+
});
3631
});
3732

3833
test('codeblock', async ({page}) => {
39-
await testMarkdownContentStyle({testContent: 'codeblock', style: 'font-family: monospace; font-size: 20px; color: black; background-color: lightgray;', page});
34+
await testMarkdownContentStyle({
35+
testContent: 'codeblock',
36+
style:
37+
'font-family: monospace; font-size: 20px; color: black; background-color: lightgray; border-color: gray; border-width: 1px; border-radius: 4px; border-style: solid; padding: 2px;',
38+
page,
39+
});
4040
});
4141

4242
test('mention-here', async ({page}) => {

WebExample/__tests__/textManipulation.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ test('cut content changes', async ({page, browserName}) => {
132132

133133
expect(await getElementValue(inputLocator)).toBe(EXPECTED_CONTENT);
134134

135-
// Ckeck if there is no markdown elements after the cut operation
135+
// Check if there is no markdown elements after the cut operation
136136
const spans = await inputLocator.locator('span[data-type="text"]');
137137
expect(await spans.count()).toBe(1);
138138
});

0 commit comments

Comments
 (0)