Skip to content
Open
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
121 changes: 65 additions & 56 deletions playground/client-navigation/__tests__/e2e.test.mts
Original file line number Diff line number Diff line change
@@ -1,62 +1,71 @@
import {
poll,
setupPlaygroundEnvironment,
testDevAndDeploy,
waitForHydration,
} from "rwsdk/e2e";
import { expect } from "vitest";

setupPlaygroundEnvironment(import.meta.url);

testDevAndDeploy("renders Hello World", async ({ page, url }) => {
await page.goto(url);

const getPageContent = () => page.content();

await poll(async () => {
const content = await getPageContent();
expect(content).toContain("Hello World");
return true;
});
});

testDevAndDeploy(
"programmatically navigates on button click",
async ({ page, url }) => {
await page.goto(url);

await waitForHydration(page);

await page.click("#navigate-to-about");

const getPageContent = () => page.content();

await poll(async () => {
const content = await getPageContent();
expect(content).toContain("About Page");
expect(content).not.toContain("Hello World");
return true;
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
import { expect, test } from "vitest";

/**
* Derive the playground directory from import.meta.url by finding the nearest package.json
*/
function getPlaygroundDirFromImportMeta(importMetaUrl: string): string {
const testFilePath = fileURLToPath(importMetaUrl);
let currentDir = path.dirname(testFilePath);
// Walk up the tree from the test file's directory
while (path.dirname(currentDir) !== currentDir) {
// Check if a package.json exists in the current directory
if (existsSync(path.join(currentDir, "package.json"))) {
return currentDir;
}
currentDir = path.dirname(currentDir);
}

throw new Error(
`Could not determine playground directory from import.meta.url: ${importMetaUrl}. ` +
`Failed to find a package.json in any parent directory.`,
);
}

test("tsc reports error for undefined route", () => {
const projectDir = getPlaygroundDirFromImportMeta(import.meta.url);

// Generate types first to ensure worker-configuration.d.ts exists
try {
execSync("pnpm generate", {
cwd: projectDir,
encoding: "utf-8",
stdio: "pipe",
});
} catch (error: any) {
// Ignore errors from generate - it might fail if wrangler isn't configured,
// but we can still test tsc if the types file exists
}

let tscOutput = "";
let tscExitCode = 0;

try {
execSync("tsc --noEmit", {
cwd: projectDir,
encoding: "utf-8",
stdio: "pipe",
});
} catch (error: any) {
tscExitCode = error.status || error.code || 1;
tscOutput = error.stdout?.toString() || error.stderr?.toString() || "";
}

expect(page.url()).toContain("/about");
},
);

testDevAndDeploy("navigates on link click", async ({ page, url }) => {
await page.goto(url);

await waitForHydration(page);

await page.click("#about-link");
// tsc should exit with a non-zero code when there are errors
expect(tscExitCode).not.toBe(0);

const getPageContent = () => page.content();
// Count the number of errors in the output
// TypeScript error messages typically start with the file path followed by a colon and line number
const errorMatches = tscOutput.match(/\.tsx?:\d+:\d+ - error/g);
const errorCount = errorMatches ? errorMatches.length : 0;

await poll(async () => {
const content = await getPageContent();
expect(content).toContain("About Page");
expect(content).not.toContain("Hello World");
return true;
});
// Should have exactly one error
expect(errorCount).toBe(1);

Check failure on line 66 in playground/client-navigation/__tests__/e2e.test.mts

View workflow job for this annotation

GitHub Actions / Playground E2E Test (ubuntu-latest, npm)

client-navigation/__tests__/e2e.test.mts > tsc reports error for undefined route

AssertionError: expected +0 to be 1 // Object.is equality - Expected + Received - 1 + 0 ❯ client-navigation/__tests__/e2e.test.mts:66:22

expect(page.url()).toContain("/about");
// The error should be about the undefined route
expect(tscOutput).toContain("/undefined-route");
expect(tscOutput).toMatch(/error TS\d+:/);
});
3 changes: 3 additions & 0 deletions playground/client-navigation/src/app/shared/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ import { linkFor } from "rwsdk/router";
type App = typeof import("../../worker").default;

export const link = linkFor<App>();

// This should cause a TypeScript error because "/undefined-route" is not defined in the worker
export const undefinedRoute = link("/undefined-route");
29 changes: 29 additions & 0 deletions playground/typed-routes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Typed Routes Playground

This playground demonstrates and tests the typed routes functionality with `defineLinks` that automatically infers routes from the app definition.

## Features Tested

- Static routes (e.g., `/`)
- Routes with named parameters (e.g., `/users/:id`)
- Routes with wildcards (e.g., `/files/*`)
- Type-safe link generation with automatic route inference
- Parameter validation at compile-time and runtime

## Running the dev server

```shell
npm run dev
```

Point your browser to the URL displayed in the terminal (e.g. `http://localhost:5173/`).

## Testing

Run the end-to-end tests from the monorepo root:

```shell
pnpm test:e2e -- playground/typed-routes/__tests__/e2e.test.mts
```


95 changes: 95 additions & 0 deletions playground/typed-routes/__tests__/e2e.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { poll, setupPlaygroundEnvironment, testDevAndDeploy } from "rwsdk/e2e";
import { expect } from "vitest";

setupPlaygroundEnvironment(import.meta.url);

testDevAndDeploy(
"renders home page with typed routes",
async ({ page, url }) => {
await page.goto(url);

await page.waitForFunction('document.readyState === "complete"');

const getPageContent = async () => await page.content();

await poll(async () => {
const content = await getPageContent();
expect(content).toContain("Typed Routes Playground");
expect(content).toContain("/");
expect(content).toContain("/users/");
expect(content).toContain("/files/");
expect(content).toContain("/blog/");
return true;
});
},
);

testDevAndDeploy("navigates to user profile page", async ({ page, url }) => {
await page.goto(url);
await page.waitForFunction('document.readyState === "complete"');

// Wait for navigation link and click it
await poll(async () => {
const userLink = await page.$('a[href*="/users/"]');
if (!userLink) return false;
await userLink.click();
return true;
});

// Wait for navigation
await page.waitForFunction('document.readyState === "complete"');

await poll(async () => {
const content = await page.content();
expect(content).toContain("User Profile");
expect(content).toContain("123");
return true;
});
});

testDevAndDeploy("navigates to file viewer page", async ({ page, url }) => {
await page.goto(url);
await page.waitForFunction('document.readyState === "complete"');

// Wait for file link and click it
await poll(async () => {
const fileLink = await page.$('a[href*="/files/"]');
if (!fileLink) return false;
await fileLink.click();
return true;
});

// Wait for navigation
await page.waitForFunction('document.readyState === "complete"');

await poll(async () => {
const content = await page.content();
expect(content).toContain("File Viewer");
expect(content).toContain("documents/readme.md");
return true;
});
});

testDevAndDeploy("navigates to blog post page", async ({ page, url }) => {
await page.goto(url);
await page.waitForFunction('document.readyState === "complete"');

// Wait for blog link and click it
await poll(async () => {
const blogLink = await page.$('a[href*="/blog/"]');
if (!blogLink) return false;
await blogLink.click();
return true;
});

// Wait for navigation
await page.waitForFunction('document.readyState === "complete"');

await poll(async () => {
const content = await page.content();
expect(content).toContain("Blog Post");
expect(content).toContain("2024");
expect(content).toContain("hello-world");
return true;
});
});
50 changes: 50 additions & 0 deletions playground/typed-routes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "typed-routes",
"version": "1.0.0",
"description": "Test playground for typed routes with defineLinks",
"main": "index.js",
"type": "module",
"keywords": [],
"author": "",
"license": "MIT",
"private": true,
"scripts": {
"build": "vite build",
"dev": "vite dev",
"dev:init": "rw-scripts dev-init",
"preview": "vite preview",
"worker:run": "rw-scripts worker-run",
"clean": "npm run clean:vite",
"clean:vite": "rm -rf ./node_modules/.vite",
"release": "rw-scripts ensure-deploy-env && npm run clean && npm run build && wrangler deploy",
"generate": "rw-scripts ensure-env && wrangler types",
"check": "npm run generate && npm run types",
"types": "tsc"
},
"dependencies": {
"rwsdk": "workspace:*",
"react": "19.3.0-canary-fb2177c1-20251114",
"react-dom": "19.3.0-canary-fb2177c1-20251114",
"react-server-dom-webpack": "19.3.0-canary-fb2177c1-20251114"
},
"devDependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@cloudflare/workers-types": "4.20251121.0",
"@types/node": "22.18.8",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"typescript": "5.9.3",
"vite": "7.2.4",
"vitest": "^3.1.1",
"wrangler": "4.50.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"sharp",
"workerd"
]
}
}


19 changes: 19 additions & 0 deletions playground/typed-routes/public/favicon-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions playground/typed-routes/public/favicon-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions playground/typed-routes/src/app/Document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import stylesUrl from "./styles.css?url";
export const Document: React.FC<{ children: React.ReactNode }> = ({
children,
}) => (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Typed Routes Playground</title>
<link rel="modulepreload" href="/src/client.tsx" />
<link rel="stylesheet" href={stylesUrl} />
<link
rel="icon"
type="image/svg+xml"
href="/favicon-dark.svg"
media="(prefers-color-scheme: dark)"
/>
<link
rel="icon"
type="image/svg+xml"
href="/favicon-light.svg"
media="(prefers-color-scheme: light)"
/>
</head>
<body>
<div id="root">{children}</div>
<script>import("/src/client.tsx")</script>
</body>
</html>
);


Loading
Loading