Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Counter as PlainCount } from '../plain-count/Counter';
import { Counter as TypedCount } from '../typed-count/Counter';
import { createDemoWithVariants } from '../../createDemo';

export const DemoMixedCounter = createDemoWithVariants(import.meta.url, {
PlainCount,
TypedCount,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from 'react';
import { DemoMixedCounter } from '.';

export default function Page() {
return (
// @focus
<DemoMixedCounter />
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as React from 'react';

export function Counter() {
return (
// @focus
<button type="button">Clicked 0 times</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import path from 'node:path';
import { test, expect } from '@playwright/test';

// The standalone demo route, derived from this file's location under `app`.
const route = path
.dirname(import.meta.filename)
.split('/app')
.pop()!;

// The first variant has no transform (its JS version only renames the file), the
// second is TypeScript with a JS transform. The highlighter considers the first
// variant current, while the JS toggle belongs to the second: switching to the
// second, then to JS, must paint each final tree at once, with no re-flow.
test('switches into the TypeScript variant and to JS without re-flowing after either swap', async ({
page,
}) => {
const pageErrors: Error[] = [];
page.on('pageerror', (error) => pageErrors.push(error));

await page.goto(`${route}/demo-plain`);
const code = page.locator('pre').first();
await expect(code).toContainText('Clicked 0 times', { timeout: 15000 });
await expect(page.getByRole('button', { name: 'JS' })).toHaveCount(0);

// Record what the code shows after every change to the page, from here on: its
// text, and the markup of the frames in its collapsed window. (Frames outside
// the window are highlighted lazily, once visible, so they may change later.)
// The observer watches the page, since a swap may replace the `pre` itself.
await page.evaluate(() => {
const snapshots: string[] = [];
(window as unknown as { preSnapshots: string[] }).preSnapshots = snapshots;
new MutationObserver(() => {
const pre = document.querySelector('pre');
const windowFrames = Array.from(pre?.querySelectorAll('.frame[data-frame-type]') ?? []);
const snapshot = `${pre?.textContent}\n${windowFrames.map((frame) => frame.outerHTML).join('')}`;
if (snapshot !== snapshots[snapshots.length - 1]) {
snapshots.push(snapshot);
}
}).observe(document.body, { childList: true, subtree: true, characterData: true });
});
const snapshots = () =>
page.evaluate(() => (window as unknown as { preSnapshots: string[] }).preSnapshots);

await page.getByText('PlainCount', { exact: true }).click();
await page.getByRole('option', { name: 'TypedCount' }).click();
await expect(code).toContainText('interface CounterProps', { timeout: 15000 });

await page.getByRole('button', { name: 'JS' }).click();
await expect(page.getByText('Counter.jsx')).toBeVisible({ timeout: 15000 });
await expect(code).not.toContainText('interface CounterProps');
await expect(code).toContainText('<output>');

// Give a late re-render time to land, then check each swap painted its final
// tree: what the code shows never changed after the variant's first TypeScript
// render, or after its first JS render.
await page.waitForTimeout(1000);
const shown = await snapshots();
const firstJs = shown.findIndex(
(snapshot) => snapshot.includes('<output>') && !snapshot.includes('CounterProps'),
);
expect(firstJs).toBeGreaterThanOrEqual(0);
expect(new Set(shown.slice(firstJs)).size).toBe(1);
const typed = shown.slice(0, firstJs).filter((snapshot) => snapshot.includes('CounterProps'));
expect(new Set(typed).size).toBe(1);

expect(pageErrors, 'the demo should swap without uncaught errors').toEqual([]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';

interface CounterProps {
count?: number;
}

export function Counter({ count = 0 }: CounterProps) {
return (
// @focus-start
<p>
Clicked <output>{count}</output> times
</p>
// @focus-end
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Counter as InlineCount } from '../inline-count/Counter';
import { Counter as OutputCount } from '../output-count/Counter';
import { createDemoWithVariants } from '../../createDemo';

export const DemoCounterInit = createDemoWithVariants(
import.meta.url,
{ InlineCount, OutputCount },
{ highlightAfter: 'init' },
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from 'react';
import { DemoCounterInit } from '.';

export default function Page() {
return (
// @focus
<DemoCounterInit />
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Counter as InlineCount } from '../inline-count/Counter';
import { Counter as OutputCount } from '../output-count/Counter';
import { createDemoWithVariants } from '../../demo-fallback/createDemo';

export const DemoCounterLoading = createDemoWithVariants(import.meta.url, {
InlineCount,
OutputCount,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from 'react';
import { DemoCounterLoading } from '.';

export default function Page() {
return (
// @focus
<DemoCounterLoading />
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Counter as InlineCount } from '../inline-count/Counter';
import { Counter as OutputCount } from '../output-count/Counter';
import { createDemoWithVariants } from '../../createDemo';

export const DemoCounterPlain = createDemoWithVariants(import.meta.url, {
InlineCount,
OutputCount,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from 'react';
import { DemoCounterPlain } from '.';

export default function Page() {
return (
// @focus
<DemoCounterPlain />
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from 'react';

interface CounterProps {
count?: number;
}

export function Counter({ count = 0 }: CounterProps) {
return (
// @focus
<button type="button">Clicked {count} times</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';

interface CounterProps {
count?: number;
}

export function Counter({ count = 0 }: CounterProps) {
return (
// @focus-start
<p>
Clicked <output>{count}</output> times
</p>
// @focus-end
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import path from 'node:path';
import { test, expect } from '@playwright/test';

// The standalone demo route, derived from this file's location under `app`.
const route = path
.dirname(import.meta.filename)
.split('/app')
.pop()!;

// Both variants name their file `Counter.tsx`, and each production payload is
// compressed with its own file's text as the DEFLATE dictionary. Switching
// variants with the JS transform applied must decode the incoming variant with
// ITS dictionary, not the same-named file's from the initial variant.
const setups = [
{ name: 'demo-plain', description: 'without a ContentLoading' },
{ name: 'demo-init', description: "with highlightAfter: 'init' and no ContentLoading" },
{ name: 'demo-loading', description: 'with a ContentLoading' },
];

for (const { name, description } of setups) {
test(`keeps the JS transform when switching between same-named variant files ${description}`, async ({
page,
}) => {
const pageErrors: Error[] = [];
const consoleErrors: string[] = [];
page.on('pageerror', (error) => pageErrors.push(error));
page.on('console', (message) => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
}
});

await page.goto(`${route}/${name}`);
const code = page.locator('pre').first();

await expect(code).toContainText('interface CounterProps', { timeout: 15000 });

await page.getByRole('button', { name: 'JS' }).click();
await expect(page.getByText('Counter.jsx')).toBeVisible({ timeout: 15000 });
await expect(code).not.toContainText('interface CounterProps');

await page.getByText('InlineCount', { exact: true }).click();
await page.getByRole('option', { name: 'OutputCount' }).click();

// The incoming variant renders, still in JavaScript.
await expect(code).toContainText('<output>', { timeout: 15000 });
await expect(code).not.toContainText('interface CounterProps');
await expect(page.getByText('Counter.jsx')).toBeVisible();

expect(
consoleErrors.filter((text) => /dictionary mismatch|Transform failed/i.test(text)),
).toEqual([]);
expect(pageErrors, 'the demo should switch variants without uncaught errors').toEqual([]);
});
}
Loading
Loading