Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
59eacd4
replace the smoke test with a harness for per-example specs
claude Oct 2, 2026
048883e
add specs for create-pages/basic and define-router/basic
claude Oct 2, 2026
7a37d21
define-router/basic: render each /dynamic/* visit
claude Oct 2, 2026
a192812
add specs for the other create-pages examples
claude Oct 2, 2026
86fa846
add specs for six minimal-api examples
claude Oct 2, 2026
5a76f89
add specs for minimal-api cookies and islands
claude Oct 2, 2026
4b5c943
add specs for minimal-api form and functions
claude Oct 2, 2026
02c0e5f
minimal-api/jotai: update the server atom in production
claude Oct 2, 2026
dc80072
minimal-api/nesting: fix the inner app refetch and /no-ssr in dev
claude Oct 2, 2026
e1089ab
add specs for minimal-api jotai, nesting and spa
claude Oct 2, 2026
9b4b9f1
add specs for fs-router basic and styling examples
claude Oct 2, 2026
6868b1d
fs-router/data-fetching: render nidoran and link to mr. mime
claude Oct 2, 2026
4c3259d
add specs for fs-router data-fetching, features, form and jotai
claude Oct 2, 2026
6d63412
add specs for fs-router cloudflare and tanstack-router
claude Oct 2, 2026
007d2c4
add specs for fs-router nextjs-photo-modal and nextjs-blog-starter
claude Oct 2, 2026
8fd65f2
add a spec for fs-router nextjs-dashboard
claude Oct 2, 2026
d878ea9
fs-router/nextjs-commerce: serve one robots tag on product pages
claude Oct 2, 2026
29b7e0a
add a spec for fs-router nextjs-commerce
claude Oct 2, 2026
91f0e61
match escaped newlines in ignoreErrors
claude Oct 2, 2026
3cf29e5
Merge remote-tracking branch 'upstream/main' into per-example-e2e
claude Oct 2, 2026
63cab9d
Merge remote-tracking branch 'upstream/main' into per-example-e2e
claude Oct 7, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- run: pnpm install
- name: Install Playwright (chromium)
run: pnpm exec playwright install --with-deps chromium
- name: Run examples smoke tests
- name: Run e2e tests
run: pnpm exec playwright test --shard=${{ matrix.shard }}/4
timeout-minutes: 30
- uses: actions/upload-artifact@v4
Expand Down
134 changes: 134 additions & 0 deletions e2e/create-pages-basic.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { expect } from '@playwright/test';
import { prepareExample, test, waitForHydration } from './utils.js';

const startApp = prepareExample('create-pages/basic');

test.describe('create-pages/basic', () => {
let port: number;
let stopApp: () => Promise<void>;
const url = (path: string) => `http://localhost:${port}${path}`;

test.beforeAll(async ({ mode }) => {
({ port, stopApp } = await startApp(mode));
});

test.afterAll(async () => {
await stopApp();
});

test('navigates between static and dynamic pages', async ({ page }) => {
await page.goto(url('/'));
await waitForHydration(page);
await expect(page).toHaveTitle('Waku');
await expect(page.getByText('This is the home page.')).toBeVisible();
await page.locator('a[href="/foo"]').click();
await expect(
page.getByRole('heading', { name: 'Foo', exact: true }),
).toBeVisible();
await expect(page.getByText('path: /foo')).toBeVisible();
await page.getByRole('button', { name: 'Increment' }).click();
await expect(page.getByText('Count: 1')).toBeVisible();
await page.getByRole('link', { name: 'Baz', exact: true }).click();
await expect(
page.getByRole('heading', { name: 'Dynamic: Baz' }),
).toBeVisible();
await expect(page).toHaveURL(url('/baz'));
});

test('renders nested layouts and static paths', async ({ page }) => {
await page.goto(url('/nested/qux'));
await expect(
page.getByRole('heading', { name: 'Nested Layout' }),
).toBeVisible();
await expect(page.getByRole('heading', { name: 'Qux' })).toBeVisible();
await page.goto(url('/nested/foo'));
await expect(
page.getByRole('heading', { name: 'Deeply Nested Layout' }),
).toBeVisible();
await expect(
page.getByRole('heading', { name: 'Static: foo' }),
).toBeVisible();
await page.goto(url('/nested/xyz'));
await expect(
page.getByRole('heading', { name: 'Dynamic: xyz' }),
).toBeVisible();
});

test('renders a new timestamp for each visit to a dynamic page', async ({
page,
}) => {
await page.goto(url('/nested/baz'));
const first = await page.locator('p').last().textContent();
expect(first).toMatch(/^\d{4}-\d{2}-\d{2}T/);
await page.reload();
await expect(page.locator('p').last()).not.toHaveText(first!);
});

test('matches wildcard and catch-all routes', async ({ page }) => {
await page.goto(url('/wild/hello/world'));
await expect(
page.getByRole('heading', { name: 'Slug: hello/world' }),
).toBeVisible();
await page.goto(url('/any/a/b'));
await expect(
page.getByRole('heading', { name: 'Catch-all: a/b' }),
).toBeVisible();
});

test('renders the custom 404 page for an unknown path', async ({ page }) => {
const res = await page.goto(url('/no-such-page'));
expect(res?.status()).toBe(404);
await expect(
page.getByRole('heading', { name: 'Not Found' }),
).toBeVisible();
await expect(page.locator('a[href="/foo"]')).toBeVisible();
});

test('renders a static slice once and a dynamic slice per request', async ({
page,
}) => {
await page.goto(url('/slice-page'));
const staticSlice = page.getByRole('heading', { name: /^Static Slice / });
await expect(staticSlice).toBeVisible();
await expect(
page.getByRole('heading', { name: /^Dynamic Slice Component / }),
).toBeVisible();
const text = await staticSlice.textContent();
await page.reload();
await expect(staticSlice).toHaveText(text!);
});

test('jumps to a random page from a server function', async ({ page }) => {
await page.goto(url('/foo'));
await waitForHydration(page);
await page.getByRole('button', { name: 'Jump to random page' }).click();
await expect(page).not.toHaveURL(url('/foo'));
const headings: Record<string, string> = {
'/bar': 'Bar',
'/baz': 'Dynamic: Baz',
'/nested/qux': 'Qux',
'/nested/aaa': 'Dynamic: aaa',
'/nested/bbb': 'Dynamic: bbb',
};
const heading = headings[new URL(page.url()).pathname];
expect(heading).toBeDefined();
await expect(
page.getByRole('heading', { name: heading, exact: true }),
).toBeVisible();
});

test('serves API routes', async ({ request }) => {
expect(await (await request.get(url('/api/hi'))).text()).toBe(
'hello world!',
);
expect(
await (await request.post(url('/api/hi'), { data: 'Waku' })).text(),
).toBe('hello Waku!');
expect(await (await request.get(url('/api/hi.txt'))).text()).toBe(
'hello from a text file!',
);
const empty = await request.get(url('/api/empty'));
expect(empty.status()).toBe(200);
expect(await empty.text()).toBe('');
});
});
28 changes: 28 additions & 0 deletions e2e/create-pages-react-tweet.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { expect } from '@playwright/test';
import { prepareExample, test } from './utils.js';

const startApp = prepareExample('create-pages/react-tweet');

test.describe('create-pages/react-tweet', () => {
let port: number;
let stopApp: () => Promise<void>;

test.beforeAll(async ({ mode }) => {
({ port, stopApp } = await startApp(mode));
});

test.afterAll(async () => {
await stopApp();
});

// The tweet is fetched from X's API, which may be unreachable, and then
// react-tweet renders its "Tweet not found" card in the same container.
test('renders the embedded tweet card', async ({ page }) => {
await page.goto(`http://localhost:${port}/`);
await expect(page).toHaveTitle('Waku');
await expect(
page.getByRole('heading', { name: 'Waku', exact: true }),
).toBeVisible();
await expect(page.locator('.react-tweet-theme article')).toBeVisible();
});
});
54 changes: 54 additions & 0 deletions e2e/create-pages-view-transitions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect } from '@playwright/test';
import { prepareExample, test, waitForHydration } from './utils.js';

const startApp = prepareExample('create-pages/view-transitions');

test.describe('create-pages/view-transitions', () => {
let port: number;
let stopApp: () => Promise<void>;

test.beforeAll(async ({ mode }) => {
({ port, stopApp } = await startApp(mode));
});

test.afterAll(async () => {
await stopApp();
});

test('runs a view transition on each navigation', async ({ page }) => {
await page.addInitScript(() => {
const startViewTransition = Document.prototype.startViewTransition;
let count = 0;
Object.defineProperty(window, 'viewTransitions', { get: () => count });
Document.prototype.startViewTransition = function (...args) {
count += 1;
return startViewTransition.apply(this, args);
};
});
const viewTransitions = () =>
page.evaluate(
() =>
(window as unknown as { viewTransitions: number }).viewTransitions,
);
await page.goto(`http://localhost:${port}/`);
await waitForHydration(page);
await expect(
page.getByRole('heading', { name: 'Waku view transitions' }),
).toBeVisible();
await page.getByRole('link', { name: 'About' }).click();
await expect(
page.getByRole('heading', { name: 'About Waku' }),
).toBeVisible();
await expect(
page.getByText(
'This is the about page with a different background color.',
),
).toBeVisible();
await expect.poll(viewTransitions).toBe(1);
await page.getByRole('link', { name: 'Home' }).click();
await expect(
page.getByRole('heading', { name: 'Waku view transitions' }),
).toBeVisible();
await expect.poll(viewTransitions).toBe(2);
});
});
54 changes: 54 additions & 0 deletions e2e/create-pages-weave-render.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect } from '@playwright/test';
import { prepareExample, test, waitForHydration } from './utils.js';

const startApp = prepareExample('create-pages/weave-render');

test.describe('create-pages/weave-render', () => {
let port: number;
let stopApp: () => Promise<void>;
const url = (path: string) => `http://localhost:${port}${path}`;

test.beforeAll(async ({ mode }) => {
({ port, stopApp } = await startApp(mode));
});

test.afterAll(async () => {
await stopApp();
});

test('keeps the static home layout across pages', async ({ page }) => {
await page.goto(url('/'));
await waitForHydration(page);
const renderTime = page.getByText(/^Last render time: /);
const text = await renderTime.textContent();
await expect(
page.getByRole('heading', { name: 'Home', exact: true }),
).toBeVisible();
await page.locator('a[href="/foo"]').first().click();
await expect(
page.getByRole('heading', { name: 'FOO layout' }),
).toBeVisible();
await expect(
page.getByRole('heading', { name: 'Foo', exact: true }),
).toBeVisible();
await expect(renderTime).toHaveText(text!);
await page.locator('a[href="/bar"]').click();
await expect(
page.getByText('This Layout is expected to be static'),
).toBeVisible();
await expect(
page.getByRole('heading', { name: 'Bar', exact: true }),
).toBeVisible();
await expect(renderTime).toHaveText(text!);
await page.reload();
await expect(renderTime).toHaveText(text!);
});

test('runs the client component in a dynamic page', async ({ page }) => {
await page.goto(url('/foo'));
await waitForHydration(page);
await expect(page.getByText('path: /foo')).toBeVisible();
await page.getByRole('button', { name: 'Increment' }).click();
await expect(page.getByText('Count: 1')).toBeVisible();
});
});
68 changes: 68 additions & 0 deletions e2e/define-router-basic.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { expect } from '@playwright/test';
import { prepareExample, test, waitForHydration } from './utils.js';

const startApp = prepareExample('define-router/basic');

test.describe('define-router/basic', () => {
let port: number;
let stopApp: () => Promise<void>;
const url = (path: string) => `http://localhost:${port}${path}`;

test.beforeAll(async ({ mode }) => {
({ port, stopApp } = await startApp(mode));
});

test.afterAll(async () => {
await stopApp();
});

test('navigates between the static routes', async ({ page }) => {
await page.goto(url('/'));
await waitForHydration(page);
await expect(page).toHaveTitle('Waku');
await expect(page.getByText('This is the home page.')).toBeVisible();
await page.locator('a[href="/foo"]').click();
await expect(
page.getByRole('heading', { name: 'Foo', exact: true }),
).toBeVisible();
await page.getByRole('button', { name: 'Increment' }).click();
await expect(page.getByText('Count: 1')).toBeVisible();
await page.getByRole('link', { name: 'Bar', exact: true }).click();
await expect(
page.getByRole('heading', { name: 'Bar', exact: true }),
).toBeVisible();
await page.getByRole('link', { name: 'Nested / Baz' }).click();
await expect(
page.getByRole('heading', { name: 'Baz', exact: true }),
).toBeVisible();
});

test('renders the dynamic route for each visited path', async ({ page }) => {
await page.goto(url('/dynamic/foo'));
await waitForHydration(page);
await expect(
page.getByRole('heading', { name: '/dynamic/foo' }),
).toBeVisible();
await page.getByRole('link', { name: 'Dynamic / bar' }).click();
await expect(
page.getByRole('heading', { name: '/dynamic/bar' }),
).toBeVisible();
await page.getByRole('link', { name: 'Dynamic / foo' }).click();
await expect(page).toHaveURL(url('/dynamic/foo'));
await expect(
page.getByRole('heading', { name: '/dynamic/foo' }),
).toBeVisible();
});

test('serves API routes', async ({ request }) => {
expect(await (await request.get(url('/api/hi'))).text()).toBe(
'hello world!',
);
expect(await (await request.get(url('/api/hi.txt'))).text()).toBe(
'hello from a text file!\n',
);
const empty = await request.get(url('/api/empty'));
expect(empty.status()).toBe(200);
expect(await empty.text()).toBe('');
});
});
Loading
Loading