Skip to content

Commit

Permalink
dev server and file system router tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nksaraf committed Aug 8, 2023
1 parent 2fffcc6 commit 4ad14d7
Show file tree
Hide file tree
Showing 12 changed files with 388 additions and 6 deletions.
6 changes: 4 additions & 2 deletions packages/vinxi/lib/dev-server.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import getPort from "get-port";
import { defineEventHandler, fromNodeMiddleware, toNodeListener } from "h3";
import { createNitro } from "nitropack";
import {
Expand Down Expand Up @@ -149,6 +150,7 @@ const routerModeDevPlugin = {
},
}),
treeShake(),
fileSystemWatcher(),
],
build: () => [
routes(),
Expand Down Expand Up @@ -197,7 +199,7 @@ async function createViteHandler(app, router, serveConfig) {
server: {
middlewareMode: true,
hmr: {
port: serveConfig.ws.port + router.index,
port: await getPort({ port: serveConfig.ws.port + router.index }),
},
},
});
Expand Down Expand Up @@ -233,7 +235,7 @@ async function createDevRouterHandler(app, router, serveConfig) {
*/
export async function createDevServer(
app,
{ port = 3000, dev = false, ws: { port: wsPort = 16000 } = {} },
{ port = 3000, dev = false, ws: { port: wsPort = null } = {} },
) {
const serveConfig = {
port,
Expand Down
1 change: 1 addition & 0 deletions packages/vinxi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"es-module-lexer": "^1.3.0",
"esbuild": "^0.18.14",
"fast-glob": "^3.3.0",
"get-port": "^6.1.2",
"h3": "1.8.0-rc.2",
"http-proxy": "^1.18.1",
"listhen": "^1.0.4",
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

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

88 changes: 88 additions & 0 deletions test/api-hmr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { expect, test } from "@playwright/test";

import type {
AppFixture,
DevFixture,
Fixture,
} from "./helpers/create-fixture.js";
import {
createDevFixture,
createFixture,
js,
} from "./helpers/create-fixture.js";
import {
PlaywrightFixture,
prettyHtml,
selectHtml,
} from "./helpers/playwright-fixture.js";

test.describe("api hmr", () => {
let fixture: DevFixture;
let appFixture: AppFixture;
// test.skip(process.env.START_ADAPTER !== "solid-start-node");

test.beforeAll(async () => {
fixture = await createDevFixture({
files: {},
});

appFixture = await fixture.createServer();
});

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

let logs: string[] = [];

test.beforeEach(({ page }) => {
page.on("console", (msg) => {
logs.push(msg.text());
});
});

test.afterEach(async () => {
await fixture.reset();
});

test("hmr api", async () => {
let res = await fixture.requestDocument("/api/hello");
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toBe("text/html");
expect(await res.text()).toBe("Hello world");

await fixture.updateFile(
"app/api/hello.ts",
js`export default function handler(event) {
return "Hello world too";
}`,
);

await new Promise((r) => setTimeout(r, 1000));

res = await fixture.requestDocument("/api/hello");
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toBe("text/html");
expect(await res.text()).toBe("Hello world too");

await fixture.updateFile(
"app/api/new.ts",
js`export default function handler(event) {
return "Hello new";
}`,
);

await new Promise((r) => setTimeout(r, 1000));

res = await fixture.requestDocument("/api/new");
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toBe("text/html");
expect(await res.text()).toBe("Hello new");

await fixture.deleteFile("app/api/new.ts");

await new Promise((r) => setTimeout(r, 1000));
res = await fixture.requestDocument("/api/new");
expect(res.status).toBe(404);
});
});
34 changes: 34 additions & 0 deletions test/helpers/create-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface FixtureInit {
}

export type Fixture = Awaited<ReturnType<typeof createFixture>>;
export type DevFixture = Awaited<ReturnType<typeof createDevFixture>>;
export type AppFixture = Awaited<
ReturnType<Awaited<ReturnType<typeof createFixture>>["createServer"]>
>;
Expand Down Expand Up @@ -105,11 +106,44 @@ export async function createDevFixture(init: FixtureInit) {
);
};

const cache = new Map<string, string | null>();

return {
projectDir,
requestDocument,
postDocument,
getBrowserAsset,
reset: async () => {
for (const [filename, prevValue] of cache.entries()) {
if (prevValue === null) {
await fse.remove(path.join(projectDir, filename));
} else {
await fse.writeFile(path.join(projectDir, filename), prevValue);
}
}
// await fse.remove(projectDir);
// projectDir = await createFixtureProject(init);
},
deleteFile: async (filename: string) => {
if (!cache.has(filename)) {
cache.set(filename, null);
}

await fse.remove(path.join(projectDir, filename));
},
updateFile: async (filename: string, content: string) => {
const prevValue = (await fse.exists(path.join(projectDir, filename)))
? await fse.readFile(path.join(projectDir, filename), {
encoding: "utf8",
})
: null;

if (!cache.has(filename)) {
cache.set(filename, prevValue);
}

await fse.writeFile(path.join(projectDir, filename), content);
},
createServer: async () => {
return {
serverUrl: `http://${ip}:${port}`,
Expand Down
128 changes: 128 additions & 0 deletions test/hmr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { expect, test } from "@playwright/test";

import type {
AppFixture,
DevFixture,
Fixture,
} from "./helpers/create-fixture.js";
import {
createDevFixture,
createFixture,
js,
} from "./helpers/create-fixture.js";
import {
PlaywrightFixture,
prettyHtml,
selectHtml,
} from "./helpers/playwright-fixture.js";

test.describe("rendering", () => {
let fixture: DevFixture;
let appFixture: AppFixture;
// test.skip(process.env.START_ADAPTER !== "solid-start-node");

test.beforeAll(async () => {
fixture = await createDevFixture({
files: {},
});

appFixture = await fixture.createServer();
});

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

let logs: string[] = [];

test.beforeEach(({ page }) => {
page.on("console", (msg) => {
logs.push(msg.text());
});
});

test.afterEach(async () => {
await fixture.reset();
});

test("hmr ssr", async () => {
let res = await fixture.requestDocument("/");
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toBe("text/html");
expect(selectHtml(await res.text(), "[data-test-id=content]")).toBe(
prettyHtml(`<h1 data-test-id="content">Hello from Vinxi</h1>`),
);

await fixture.updateFile(
"app/root.tsx",
js`import { Counter } from './Counter';
export default function App({ assets }) {
return (
<html lang="en">
<head>
<link rel="icon" href="/favicon.ico" />
{assets}
</head>
<body>
<section>
<h1 data-test-id="content">Hello from Vinxi too</h1>
<Counter />
</section>
</body>
</html>
);
}`,
);

await new Promise((r) => setTimeout(r, 1000));

res = await fixture.requestDocument("/");
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toBe("text/html");
expect(selectHtml(await res.text(), "[data-test-id=content]")).toBe(
prettyHtml(`<h1 data-test-id="content">Hello from Vinxi too</h1>`),
);
});

test("client hmr", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/", true);

expect(await app.getHtml("[data-test-id=content]")).toBe(
prettyHtml(`<h1 data-test-id="content">Hello from Vinxi</h1>`),
);
expect(await app.getHtml("[data-test-id=count]")).toBe(
prettyHtml(`<span data-test-id="count">0</span>`),
);

await app.clickElement("[data-test-id=button]");

expect(await app.getHtml("[data-test-id=count]")).toBe(
prettyHtml(`<span data-test-id="count">1</span>`),
);

await fixture.updateFile(
"app/Counter.tsx",
js`import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<button data-test-id="button" onClick={() => setCount(count + 1)}>
Click me again
</button>
<span data-test-id="count">{count}</span>
</div>
);
}`,
);

await new Promise((r) => setTimeout(r, 1000));

expect(await app.getHtml("[data-test-id=button]")).toBe(
prettyHtml(`<button data-test-id="button">Click me again</button>`),
);
});
});
53 changes: 53 additions & 0 deletions test/templates/react/app.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
import reactRefresh from "@vitejs/plugin-react";
import { join } from "path";
import { createApp } from "vinxi";
import {
BaseFileSystemRouter,
analyzeModule,
cleanPath,
} from "vinxi/file-system-router";

class APIFileSystemRouter extends BaseFileSystemRouter {
toPath(src) {
const routePath = cleanPath(src, this.config)
// remove the initial slash
.slice(1)
.replace(/index$/, "")
.replace(/\[([^\/]+)\]/g, (_, m) => {
if (m.length > 3 && m.startsWith("...")) {
return `:${m.slice(3)}*`;
}
if (m.length > 2 && m.startsWith("[") && m.endsWith("]")) {
return `:${m.slice(1, -1)}?`;
}
return `:${m}`;
});

return routePath?.length > 0 ? `/${routePath}` : "/";
}

toRoute(src) {
let path = this.toPath(src);

const [_, exports] = analyzeModule(src);
const hasDefault = exports.find((e) => e.n === "default");
return {
$handler: {
src: src,
pick: ["default"],
},
path,
filePath: src,
};
}
}

export default createApp({
routers: [
Expand All @@ -9,6 +50,18 @@ export default createApp({
dir: "./public",
base: "/",
},
{
name: "api",
mode: "handler",
handler: "./app/api.ts",
dir: "./app/api",
style: APIFileSystemRouter,
build: {
target: "node",
// plugins: () => [reactRefresh()],
},
base: "/api",
},
{
name: "client",
mode: "build",
Expand Down
Loading

0 comments on commit 4ad14d7

Please sign in to comment.