Skip to content

Commit

Permalink
test: catch an error in client router (#776)
Browse files Browse the repository at this point in the history
It covers #696 and #741.
The test aims at how to implement auth features on the app side.

Considerations:
1. Our client router has to handle 4xx errors that the server responds
2. Middleware can be a validator to respond to 200 or 4xx status on
requests

I'm so glad to send my first PR to Waku, thank you!
  • Loading branch information
t6adev authored Jul 10, 2024
1 parent b603c76 commit 7388766
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 1 deletion.
23 changes: 23 additions & 0 deletions e2e/fixtures/ssr-catch-error/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "ssr-catch-error",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "waku dev",
"build": "waku build",
"start": "waku start"
},
"dependencies": {
"react": "19.0.0-rc-3da26163a3-20240704",
"react-dom": "19.0.0-rc-3da26163a3-20240704",
"react-error-boundary": "4.0.13",
"react-server-dom-webpack": "19.0.0-rc-3da26163a3-20240704",
"waku": "workspace:*"
},
"devDependencies": {
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"typescript": "5.5.3"
}
}
24 changes: 24 additions & 0 deletions e2e/fixtures/ssr-catch-error/src/components/client-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import type { ReactNode } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

const FallbackComponent = ({ error }: { error: any }) => {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{ color: 'red' }}>{error.message}</pre>
{error.statusCode && (
<pre style={{ color: 'red' }}>{error.statusCode}</pre>
)}
</div>
);
};

export const ClientLayout = ({ children }: { children: ReactNode }) => {
return (
<ErrorBoundary FallbackComponent={FallbackComponent}>
{children}
</ErrorBoundary>
);
};
30 changes: 30 additions & 0 deletions e2e/fixtures/ssr-catch-error/src/middleware/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Middleware } from 'waku/config';
import wakuConfig from '../../waku.config.js';

const { rscPath } = wakuConfig;

const stringToStream = (str: string): ReadableStream => {
const encoder = new TextEncoder();
return new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(str));
controller.close();
},
});
};

const validateMiddleware: Middleware = () => {
return async (ctx, next) => {
if (
ctx.req.url.pathname === '/invalid' ||
ctx.req.url.pathname.startsWith(`/${rscPath}/invalid`)
) {
ctx.res.status = 401;
ctx.res.body = stringToStream('Unauthorized');
return;
}
await next();
};
};

export default validateMiddleware;
16 changes: 16 additions & 0 deletions e2e/fixtures/ssr-catch-error/src/pages/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ReactNode } from 'react';
import { ClientLayout } from '../components/client-layout.js';

export default async function RootLayout({
children,
}: {
children: ReactNode;
}) {
return <ClientLayout>{children}</ClientLayout>;
}

export const getConfig = async () => {
return {
render: 'static',
};
};
16 changes: 16 additions & 0 deletions e2e/fixtures/ssr-catch-error/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Link } from 'waku';

export default async function HomePage() {
return (
<div>
<p>Home Page</p>
<Link to="/invalid">Invalid page</Link>
</div>
);
}

export const getConfig = async () => {
return {
render: 'static',
};
};
13 changes: 13 additions & 0 deletions e2e/fixtures/ssr-catch-error/src/pages/invalid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default async function InvalidPage() {
return (
<div>
<p>Invalid Page</p>
</div>
);
}

export const getConfig = async () => {
return {
render: 'dynamic',
};
};
17 changes: 17 additions & 0 deletions e2e/fixtures/ssr-catch-error/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"composite": true,
"strict": true,
"target": "esnext",
"downlevelIteration": true,
"esModuleInterop": true,
"module": "nodenext",
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"types": ["react/experimental"],
"jsx": "react-jsx",
"outDir": "./dist"
},
"include": ["./src", "./waku.config.ts"]
}
22 changes: 22 additions & 0 deletions e2e/fixtures/ssr-catch-error/waku.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const DO_NOT_BUNDLE = '';

/** @type {import('waku/config').Config} */
export default {
middleware: (cmd: 'dev' | 'start') => [
...(cmd === 'dev'
? [
import(
/* @vite-ignore */ DO_NOT_BUNDLE + 'waku/middleware/dev-server'
),
]
: []),
import('./src/middleware/validator.js'),
import('waku/middleware/rsc'),
import('waku/middleware/fallback'),
],
/**
* Prefix for HTTP requests to indicate RSC requests.
* Defaults to "RSC".
*/
rscPath: 'RSC', // Just for clarification in tests
};
69 changes: 69 additions & 0 deletions e2e/ssr-catch-error.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { expect } from '@playwright/test';
import { execSync, exec, ChildProcess } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import waitPort from 'wait-port';
import { debugChildProcess, getFreePort, terminate, test } from './utils.js';
import { rm } from 'node:fs/promises';

const waku = fileURLToPath(
new URL('../packages/waku/dist/cli.js', import.meta.url),
);

const commands = [
{
command: 'dev',
},
{
build: 'build',
command: 'start',
},
];

const cwd = fileURLToPath(
new URL('./fixtures/ssr-catch-error', import.meta.url),
);

for (const { build, command } of commands) {
test.describe(`ssr-catch-error: ${command}`, () => {
let cp: ChildProcess;
let port: number;
test.beforeAll('remove cache', async () => {
await rm(`${cwd}/dist`, {
recursive: true,
force: true,
});
});

test.beforeAll(async () => {
if (build) {
execSync(`node ${waku} ${build}`, { cwd });
}
port = await getFreePort();
cp = exec(`node ${waku} ${command} --port ${port}`, { cwd });
debugChildProcess(cp, fileURLToPath(import.meta.url), [
/ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time/,
]);
await waitPort({ port });
});

test.afterAll(async () => {
await terminate(cp.pid!);
});

test('access top page', async ({ page }) => {
await page.goto(`http://localhost:${port}/`);
await expect(page.getByText('Home Page')).toBeVisible();
});

test('access invalid page through client router', async ({ page }) => {
await page.goto(`http://localhost:${port}/`);
await page.getByText('Invalid page').click();
await expect(page.getByText('401')).toBeVisible();
});

test('access invalid page directly', async ({ page }) => {
await page.goto(`http://localhost:${port}/invalid`);
await expect(page.getByText('Unauthorized')).toBeVisible();
});
});
}
40 changes: 39 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tsconfig.e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
},
{
"path": "./e2e/fixtures/partial-build/tsconfig.json"
},
{
"path": "./e2e/fixtures/ssr-catch-error/tsconfig.json"
}
]
}

0 comments on commit 7388766

Please sign in to comment.