@samitouri / QOS-React / commits / 1eca9a2747

[playground] Add compiler playground tests (#34528)

<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory. Before submitting a pull request, please make sure the following is done: 1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`. 2. Run `yarn` in the repository root. 3. If you've fixed a bug or added code that should be tested, add tests! 4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development. 5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`. 6. If you need a debugger, run `yarn test --debug --watch TestName`, open `chrome://inspect`, and press "Inspect". 7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`). 8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files. 9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`). 10. If you haven't already, complete the CLA. Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html --> ## Summary <!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? --> Added more tests for the compiler playground with the addition of the new config editor and "Show Internals" button. Added testing to check for incomplete store params in the URL, toggle functionality, and correct errors showing for syntax/validation errors in the config overrides.

Eugene Choi committed Sep 22, 2025 at 12:11 UTC 1eca9a2747bfbe9832c3ef1665642e12d7d87d1e
7 files changed +184 -15
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/default-config.txt new
+5
@@ -0,0 +1,5 @@
1 +import type { PluginOptions } from 
2 +'babel-plugin-react-compiler/dist';
3 +({
4 +  //compilationMode: "all"
5 +} satisfies Partial<PluginOptions>);
\ No newline at end of file
compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/disableMemoizationForDebugging-output.txt new
+14
@@ -0,0 +1,14 @@
1 +import { c as _c } from "react/compiler-runtime";
2 +export default function TestComponent(t0) {
3 + const $ = _c(2);
4 + const { x } = t0;
5 + let t1;
6 + if ($[0] !== x || true) {
7 + t1 = <Button>{x}</Button>;
8 + $[0] = x;
9 + $[1] = t1;
10 + } else {
11 + t1 = $[1];
12 + }
13 + return t1;
14 +}
compiler/apps/playground/__tests__/e2e/page.spec.ts
+159 -13
@@ -5,8 +5,9 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {expect, test} from '@playwright/test';
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 {
@@ -20,6 +21,15 @@ 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 + expandButton.click();
27 +}
28 +
29 +const TEST_SOURCE = `export default function TestComponent({ x }) {
30 + return <Button>{x}</Button>;
31 +}`;
32 +
33 const TEST_CASE_INPUTS = [
34 {
35 name: 'module-scope-use-memo',
@@ -121,10 +131,9 @@ test('editor should open successfully', async ({page}) => {
131
132 test('editor should compile from hash successfully', async ({page}) => {
133 const store: Store = {
124 - source: `export default function TestComponent({ x }) {
125 - return <Button>{x}</Button>;
126 - }
127 - `,
134 + source: TEST_SOURCE,
135 + config: defaultConfig,
136 + showInternals: false,
137 };
138 const hash = encodeStore(store);
139 await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -136,7 +145,7 @@ test('editor should compile from hash successfully', async ({page}) => {
145 path: 'test-results/01-compiles-from-hash.png',
146 });
147 const text =
139 - (await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? [];
148 + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
149 const output = await formatPrint(text);
150
151 expect(output).not.toEqual('');
@@ -145,10 +154,9 @@ test('editor should compile from hash successfully', async ({page}) => {
154
155 test('reset button works', async ({page}) => {
156 const store: Store = {
148 - source: `export default function TestComponent({ x }) {
149 - return <Button>{x}</Button>;
150 - }
151 - `,
157 + source: TEST_SOURCE,
158 + config: defaultConfig,
159 + showInternals: false,
160 };
161 const hash = encodeStore(store);
162 await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -157,33 +165,171 @@ test('reset button works', async ({page}) => {
165 // Reset button works
166 page.on('dialog', dialog => dialog.accept());
167 await page.getByRole('button', {name: 'Reset'}).click();
168 + await expandConfigs(page);
169 +
170 await page.screenshot({
171 fullPage: true,
172 path: 'test-results/02-reset-button-works.png',
173 });
174 const text =
165 - (await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? [];
175 + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
176 const output = await formatPrint(text);
177
178 + const configText =
179 + (await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
180 + const configOutput = configText.join('');
181 +
182 expect(output).not.toEqual('');
183 expect(output).toMatchSnapshot('02-default-output.txt');
184 + expect(configOutput).not.toEqual('');
185 + expect(configOutput).toMatchSnapshot('default-config.txt');
186 +});
187 +
188 +test('defaults load when only source is in Store', async ({page}) => {
189 + // Test for backwards compatibility
190 + const partial = {
191 + source: TEST_SOURCE,
192 + };
193 + const hash = encodeStore(partial as Store);
194 + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
195 + await page.waitForFunction(isMonacoLoaded);
196 + await expandConfigs(page);
197 +
198 + await page.screenshot({
199 + fullPage: true,
200 + path: 'test-results/03-missing-defaults.png',
201 + });
202 +
203 + // Config editor has default config
204 + const configText =
205 + (await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
206 + const configOutput = configText.join('');
207 +
208 + expect(configOutput).not.toEqual('');
209 + expect(configOutput).toMatchSnapshot('default-config.txt');
210 +
211 + const checkbox = page.locator('label.show-internals');
212 + await expect(checkbox).not.toBeChecked();
213 + const ssaTab = page.locator('text=SSA');
214 + await expect(ssaTab).not.toBeVisible();
215 +});
216 +
217 +test('show internals button toggles correctly', async ({page}) => {
218 + await page.goto(`/`, {waitUntil: 'networkidle'});
219 + await page.waitForFunction(isMonacoLoaded);
220 +
221 + // show internals should be off
222 + const checkbox = page.locator('label.show-internals');
223 + await checkbox.click();
224 +
225 + await page.screenshot({
226 + fullPage: true,
227 + path: 'test-results/04-show-internals-on.png',
228 + });
229 +
230 + await expect(checkbox).toBeChecked();
231 +
232 + const ssaTab = page.locator('text=SSA');
233 + await expect(ssaTab).toBeVisible();
234 +});
235 +
236 +test('error is displayed when config has syntax error', async ({page}) => {
237 + const store: Store = {
238 + source: TEST_SOURCE,
239 + config: `compilationMode: `,
240 + showInternals: false,
241 + };
242 + const hash = encodeStore(store);
243 + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
244 + await page.waitForFunction(isMonacoLoaded);
245 + await expandConfigs(page);
246 + await page.screenshot({
247 + fullPage: true,
248 + path: 'test-results/05-config-syntax-error.png',
249 + });
250 +
251 + const text =
252 + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
253 + const output = text.join('');
254 +
255 + // Remove hidden chars
256 + expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
257 +});
258 +
259 +test('error is displayed when config has validation error', async ({page}) => {
260 + const store: Store = {
261 + source: TEST_SOURCE,
262 + config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
263 +
264 +({
265 + compilationMode: "123"
266 +} satisfies Partial<PluginOptions>);`,
267 + showInternals: false,
268 + };
269 + const hash = encodeStore(store);
270 + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
271 + await page.waitForFunction(isMonacoLoaded);
272 + await expandConfigs(page);
273 + await page.screenshot({
274 + fullPage: true,
275 + path: 'test-results/06-config-validation-error.png',
276 + });
277 +
278 + const text =
279 + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
280 + const output = text.join('');
281 +
282 + expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
283 +});
284 +
285 +test('disableMemoizationForDebugging flag works as expected', async ({
286 + page,
287 +}) => {
288 + const store: Store = {
289 + source: TEST_SOURCE,
290 + config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
291 +
292 +({
293 + environment: {
294 + disableMemoizationForDebugging: true
295 + }
296 +} satisfies Partial<PluginOptions>);`,
297 + showInternals: false,
298 + };
299 + const hash = encodeStore(store);
300 + await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
301 + await page.waitForFunction(isMonacoLoaded);
302 + await expandConfigs(page);
303 + await page.screenshot({
304 + fullPage: true,
305 + path: 'test-results/07-config-disableMemoizationForDebugging-flag.png',
306 + });
307 +
308 + const text =
309 + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
310 + const output = await formatPrint(text);
311 +
312 + expect(output).not.toEqual('');
313 + expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
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,
182 - path: `test-results/03-0${idx}-${t.name}.png`,
328 + path: `test-results/08-0${idx}-${t.name}.png`,
329 });
330
331 const text =
186 - (await page.locator('.monaco-editor').nth(3).allInnerTexts()) ?? [];
332 + (await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
333 let output: string;
334 if (t.noFormat) {
335 output = text.join('');
compiler/apps/playground/components/Editor/ConfigEditor.tsx
+3 -1
@@ -9,7 +9,7 @@ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
9 import {PluginOptions} from 'babel-plugin-react-compiler';
10 import type {editor} from 'monaco-editor';
11 import * as monaco from 'monaco-editor';
12 -import React, {useState, useRef, useEffect} from 'react';
12 +import React, {useState, useRef} from 'react';
13 import {Resizable} from 're-resizable';
14 import {useStore, useStoreDispatch} from '../StoreContext';
15 import {monacoOptions} from './monacoOptions';
@@ -145,6 +145,7 @@ function ExpandedEditor({
145 onMount={handleMount}
146 onChange={handleChange}
147 loading={''}
148 + className="monaco-editor-config"
149 options={{
150 ...monacoOptions,
151 lineNumbers: 'off',
@@ -170,6 +171,7 @@ function ExpandedEditor({
171 language={'javascript'}
172 value={formattedAppliedOptions}
173 loading={''}
174 + className="monaco-editor-applied-config"
175 options={{
176 ...monacoOptions,
177 lineNumbers: 'off',
compiler/apps/playground/components/Editor/Input.tsx
+1
@@ -145,6 +145,7 @@ export default function Input({errors, language}: Props): JSX.Element {
145 value={store.source}
146 onMount={handleMount}
147 onChange={handleChange}
148 + className="monaco-editor-input"
149 options={monacoOptions}
150 loading={''}
151 />
compiler/apps/playground/components/Editor/Output.tsx
+1
@@ -340,6 +340,7 @@ function TextTabContent({
340 language={language ?? 'javascript'}
341 value={output}
342 loading={''}
343 + className="monaco-editor-output"
344 options={{
345 ...monacoOptions,
346 readOnly: true,
compiler/apps/playground/components/Header.tsx
+1 -1
@@ -58,7 +58,7 @@ export default function Header(): JSX.Element {
58 </div>
59 <div className="flex items-center text-[15px] gap-4">
60 <div className="flex items-center gap-2">
61 - <label className="relative inline-block w-[34px] h-5">
61 + <label className="show-internals relative inline-block w-[34px] h-5">
62 <input
63 type="checkbox"
64 checked={store.showInternals}