main
ts 343 lines 8.79 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import {expect, test, type Page} from '@playwright/test';
9 import {encodeStore, type Store} from '../../lib/stores';
10 import {defaultConfig} from '../../lib/defaultStore';
11 import {format} from 'prettier';
12
13 function isMonacoLoaded(): boolean {
14 return (
15 typeof window['MonacoEnvironment'] !== 'undefined' &&
16 window['__MONACO_LOADED__'] === true
17 );
18 }
19
20 function formatPrint(data: Array<string>): Promise<string> {
21 return format(data.join(''), {parser: 'babel'});
22 }
23
24 async function expandConfigs(page: Page): Promise<void> {
25 const expandButton = page.locator('[title="Expand config editor"]');
26 await expandButton.click();
27 await page.waitForSelector('.monaco-editor-config', {state: 'visible'});
28 }
29
30 const TEST_SOURCE = `export default function TestComponent({ x }) {
31 return <Button>{x}</Button>;
32 }`;
33
34 const TEST_CASE_INPUTS = [
35 {
36 name: 'module-scope-use-memo',
37 input: `
38 'use memo';
39 export default function TestComponent({ x }) {
40 return <Button>{x}</Button>;
41 }`,
42 },
43 {
44 name: 'module-scope-use-no-memo',
45 input: `
46 'use no memo';
47 export default function TestComponent({ x }) {
48 return <Button>{x}</Button>;
49 }`,
50 },
51 {
52 name: 'use-memo',
53 input: `
54 function TestComponent({ x }) {
55 'use memo';
56 return <Button>{x}</Button>;
57 }
58 const TestComponent2 = ({ x }) => {
59 'use memo';
60 return <Button>{x}</Button>;
61 };`,
62 },
63 {
64 name: 'use-no-memo',
65 input: `
66 const TestComponent = function() {
67 'use no memo';
68 return <Button>{x}</Button>;
69 };
70 const TestComponent2 = ({ x }) => {
71 'use no memo';
72 return <Button>{x}</Button>;
73 };`,
74 },
75 {
76 name: 'todo-function-scope-does-not-beat-module-scope',
77 input: `
78 'use no memo';
79 function TestComponent({ x }) {
80 'use memo';
81 return <Button>{x}</Button>;
82 }`,
83 },
84 {
85 name: 'parse-typescript',
86 input: `
87 function Foo() {
88 const x = foo() as number;
89 return <div>{x}</div>;
90 }
91 `,
92 noFormat: true,
93 },
94 {
95 name: 'parse-flow',
96 input: `
97 // @flow
98 function useFoo(propVal: {+baz: number}) {
99 return <div>{(propVal.baz as number)}</div>;
100 }
101 `,
102 noFormat: true,
103 },
104 {
105 name: 'compilationMode-infer',
106 input: `// @compilationMode:"infer"
107 function nonReactFn() {
108 return {};
109 }
110 `,
111 noFormat: true,
112 },
113 {
114 name: 'compilationMode-all',
115 input: `// @compilationMode:"all"
116 function nonReactFn() {
117 return {};
118 }
119 `,
120 noFormat: true,
121 },
122 ];
123
124 test('editor should open successfully', async ({page}) => {
125 await page.goto(`/`, {waitUntil: 'networkidle'});
126 await page.waitForFunction(isMonacoLoaded);
127 await page.screenshot({
128 fullPage: true,
129 path: 'test-results/00-fresh-page.png',
130 });
131 });
132
133 test('editor should compile from hash successfully', async ({page}) => {
134 const store: Store = {
135 source: TEST_SOURCE,
136 config: defaultConfig,
137 showInternals: false,
138 };
139 const hash = encodeStore(store);
140 await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
141 await page.waitForFunction(isMonacoLoaded);
142
143 // User input from hash compiles
144 await page.screenshot({
145 fullPage: true,
146 path: 'test-results/01-compiles-from-hash.png',
147 });
148 const text =
149 (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
150 const output = await formatPrint(text);
151
152 expect(output).not.toEqual('');
153 expect(output).toMatchSnapshot('01-user-output.txt');
154 });
155
156 test('reset button works', async ({page}) => {
157 const store: Store = {
158 source: TEST_SOURCE,
159 config: defaultConfig,
160 showInternals: false,
161 };
162 const hash = encodeStore(store);
163 await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
164 await page.waitForFunction(isMonacoLoaded);
165
166 // Reset button works
167 page.on('dialog', dialog => dialog.accept());
168 await page.getByRole('button', {name: 'Reset'}).click();
169 await expandConfigs(page);
170
171 await page.screenshot({
172 fullPage: true,
173 path: 'test-results/02-reset-button-works.png',
174 });
175 const text =
176 (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
177 const output = await formatPrint(text);
178
179 const configText =
180 (await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
181 const configOutput = configText.join('');
182
183 expect(output).not.toEqual('');
184 expect(output).toMatchSnapshot('02-default-output.txt');
185 expect(configOutput).not.toEqual('');
186 expect(configOutput).toMatchSnapshot('default-config.txt');
187 });
188
189 test('defaults load when only source is in Store', async ({page}) => {
190 // Test for backwards compatibility
191 const partial = {
192 source: TEST_SOURCE,
193 };
194 const hash = encodeStore(partial as Store);
195 await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
196 await page.waitForFunction(isMonacoLoaded);
197 await expandConfigs(page);
198
199 await page.screenshot({
200 fullPage: true,
201 path: 'test-results/03-missing-defaults.png',
202 });
203
204 // Config editor has default config
205 const configText =
206 (await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
207 const configOutput = configText.join('');
208
209 expect(configOutput).not.toEqual('');
210 expect(configOutput).toMatchSnapshot('default-config.txt');
211
212 const checkbox = page.locator('label.show-internals');
213 await expect(checkbox).not.toBeChecked();
214 const ssaTab = page.locator('text=SSA');
215 await expect(ssaTab).not.toBeVisible();
216 });
217
218 test('show internals button toggles correctly', async ({page}) => {
219 await page.goto(`/`, {waitUntil: 'networkidle'});
220 await page.waitForFunction(isMonacoLoaded);
221
222 // show internals should be off
223 const checkbox = page.locator('label.show-internals');
224 await checkbox.click();
225
226 await page.screenshot({
227 fullPage: true,
228 path: 'test-results/04-show-internals-on.png',
229 });
230
231 await expect(checkbox).toBeChecked();
232
233 const ssaTab = page.locator('text=SSA');
234 await expect(ssaTab).toBeVisible();
235 });
236
237 test('error is displayed when config has syntax error', async ({page}) => {
238 const store: Store = {
239 source: TEST_SOURCE,
240 config: `{ compilationMode: }`,
241 showInternals: false,
242 };
243 const hash = encodeStore(store);
244 await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
245 await page.waitForFunction(isMonacoLoaded);
246 await expandConfigs(page);
247 await page.screenshot({
248 fullPage: true,
249 path: 'test-results/05-config-syntax-error.png',
250 });
251
252 const text =
253 (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
254 const output = text.join('');
255
256 // Remove hidden chars
257 expect(output.replace(/\s+/g, ' ')).toContain(
258 'Unexpected failure when transforming configs',
259 );
260 });
261
262 test('error is displayed when config has validation error', async ({page}) => {
263 const store: Store = {
264 source: TEST_SOURCE,
265 config: `{
266 compilationMode: "123"
267 }`,
268 showInternals: false,
269 };
270 const hash = encodeStore(store);
271 await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
272 await page.waitForFunction(isMonacoLoaded);
273 await expandConfigs(page);
274 await page.screenshot({
275 fullPage: true,
276 path: 'test-results/06-config-validation-error.png',
277 });
278
279 const text =
280 (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
281 const output = text.join('');
282
283 expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
284 });
285
286 test('error is displayed when source has syntax error', async ({page}) => {
287 const syntaxErrorSource = `function TestComponent(props) {
288 const oops = props.
289 return (
290 <>{oops}</>
291 );
292 }`;
293 const store: Store = {
294 source: syntaxErrorSource,
295 config: defaultConfig,
296 showInternals: false,
297 };
298 const hash = encodeStore(store);
299 await page.goto(`/#${hash}`);
300 await page.waitForFunction(isMonacoLoaded);
301 await expandConfigs(page);
302 await page.screenshot({
303 fullPage: true,
304 path: 'test-results/08-source-syntax-error.png',
305 });
306
307 const text =
308 (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
309 const output = text.join('');
310
311 expect(output.replace(/\s+/g, ' ')).toContain(
312 'Expected identifier to be defined before being used',
313 );
314 });
315
316 TEST_CASE_INPUTS.forEach((t, idx) =>
317 test(`playground compiles: ${t.name}`, async ({page}) => {
318 const store: Store = {
319 source: t.input,
320 config: defaultConfig,
321 showInternals: false,
322 };
323 const hash = encodeStore(store);
324 await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
325 await page.waitForFunction(isMonacoLoaded);
326 await page.screenshot({
327 fullPage: true,
328 path: `test-results/08-0${idx}-${t.name}.png`,
329 });
330
331 const text =
332 (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
333 let output: string;
334 if (t.noFormat) {
335 output = text.join('');
336 } else {
337 output = await formatPrint(text);
338 }
339
340 expect(output).not.toEqual('');
341 expect(output).toMatchSnapshot(`${t.name}-output.txt`);
342 }),
343 );