@sigil-dev/grimoire 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.grimoire/_routes.dom.js +8 -0
- package/.grimoire/_routes.hydrate.js +8 -0
- package/.grimoire/tsconfig.generated.json +11 -0
- package/.grimoire/types/ambient.d.ts +59 -0
- package/.grimoire/types/api/hello/$types.d.ts +50 -0
- package/.grimoire/types/api/items/$types.d.ts +50 -0
- package/.grimoire/types/echo/$types.d.ts +50 -0
- package/.grimoire/types/env-private.d.ts +5 -0
- package/.grimoire/types/env-public.d.ts +5 -0
- package/.grimoire/types/mixed/$types.d.ts +50 -0
- package/.grimoire/types/params/[docId]/$types.d.ts +52 -0
- package/.grimoire/types/reject/$types.d.ts +50 -0
- package/index.ts +34 -34
- package/package.json +8 -4
- package/preload.js +2 -0
- package/public/__grimoire__/hydrate.js +585 -0
- package/public/__grimoire__/index.js +490 -0
- package/src/client/head.ts +29 -0
- package/src/client/router.ts +224 -76
- package/src/env/index.ts +25 -0
- package/src/env/plugin.ts +13 -0
- package/src/env/private.ts +5 -0
- package/src/env/public.ts +7 -0
- package/src/env/typegen.ts +51 -0
- package/src/integrations/vite.ts +72 -72
- package/src/rendering/head.ts +22 -2
- package/src/rendering/hydrate.ts +81 -26
- package/src/rendering/index.ts +199 -186
- package/src/rendering/ssrPlugin.ts +53 -42
- package/src/routing/manifest-gen.ts +39 -26
- package/src/routing/router.ts +106 -98
- package/src/routing/scanner.ts +135 -129
- package/src/routing/transform-routes.ts +101 -96
- package/src/server/build.ts +147 -90
- package/src/server/coordinator.ts +306 -297
- package/src/server/hooks.ts +24 -3
- package/src/server/index.ts +148 -71
- package/src/server/worker.ts +59 -59
- package/src/typegen/index.ts +353 -340
- package/src/types.ts +269 -260
- package/test/context.test.ts +52 -52
- package/test/hydration.test.ts +119 -119
- package/test/middleware.test.ts +223 -221
- package/test/rendering.test.ts +425 -425
- package/test/routing.test.ts +83 -45
- package/test/scanning.test.ts +181 -169
- package/test/server.test.ts +229 -229
- package/test/streaming.test.ts +106 -106
- package/test/transform-routes.test.ts +84 -84
- package/test/typegen.test.ts +19 -1
package/test/server.test.ts
CHANGED
|
@@ -1,229 +1,229 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, rm, writeFile } from "fs/promises";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import { createServer } from "../src/server";
|
|
6
|
-
|
|
7
|
-
// ── Sandbox note ─────────────────────────────────────────────────────────────
|
|
8
|
-
// The sigil babel plugin unconditionally inserts `import ... from "@sigil-dev/runtime"`
|
|
9
|
-
// for all non-SSR route files. When createServer() calls Bun.build to bundle hydrate.js,
|
|
10
|
-
// those compiled page files pull in @sigil-dev/runtime → Bun's test sandbox blocks the
|
|
11
|
-
// read of packages/runtime/index.ts.
|
|
12
|
-
//
|
|
13
|
-
// Consequence: createServer() integration tests can only use +server.ts API routes
|
|
14
|
-
// (which are never bundled by Bun.build). Form action behavior at the HTTP level cannot
|
|
15
|
-
// be tested here — it is covered at unit level by fail.test.ts and redirect-error.test.ts,
|
|
16
|
-
// and at the renderer level by rendering.test.ts.
|
|
17
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
let tmpDir: string;
|
|
20
|
-
|
|
21
|
-
beforeAll(async () => {
|
|
22
|
-
tmpDir = join(tmpdir(), `grimoire-server-${Date.now()}`);
|
|
23
|
-
await mkdir(join(tmpDir, "api", "hello"), { recursive: true });
|
|
24
|
-
await mkdir(join(tmpDir, "api", "items"), { recursive: true });
|
|
25
|
-
|
|
26
|
-
await writeFile(
|
|
27
|
-
join(tmpDir, "api", "hello", "+server.ts"),
|
|
28
|
-
`export async function GET() { return Response.json({ message: "hello" }); }`,
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
await writeFile(
|
|
32
|
-
join(tmpDir, "api", "items", "+server.ts"),
|
|
33
|
-
`export async function GET() {
|
|
34
|
-
return Response.json([{ id: 1, name: "sword" }]);
|
|
35
|
-
}
|
|
36
|
-
export async function POST({ request }) {
|
|
37
|
-
const body = await request.json();
|
|
38
|
-
return Response.json({ id: 2, ...body }, { status: 201 });
|
|
39
|
-
}`,
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("Server — API routes (+server.ts)", () => {
|
|
44
|
-
test("GET returns JSON", async () => {
|
|
45
|
-
const server = await createServer({ port: 3004, routes: tmpDir });
|
|
46
|
-
const res = await fetch("http://localhost:3004/api/hello");
|
|
47
|
-
const data = await res.json();
|
|
48
|
-
expect(data.message).toBe("hello");
|
|
49
|
-
server.stop();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("GET on multi-method route returns correct JSON", async () => {
|
|
53
|
-
const server = await createServer({ port: 3004, routes: tmpDir });
|
|
54
|
-
const res = await fetch("http://localhost:3004/api/items");
|
|
55
|
-
expect(res.status).toBe(200);
|
|
56
|
-
const items = await res.json();
|
|
57
|
-
expect(items[0].name).toBe("sword");
|
|
58
|
-
server.stop();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("POST returns 201 with created resource", async () => {
|
|
62
|
-
const server = await createServer({ port: 3004, routes: tmpDir });
|
|
63
|
-
const res = await fetch("http://localhost:3004/api/items", {
|
|
64
|
-
method: "POST",
|
|
65
|
-
headers: { "Content-Type": "application/json" },
|
|
66
|
-
body: JSON.stringify({ name: "shield" }),
|
|
67
|
-
});
|
|
68
|
-
expect(res.status).toBe(201);
|
|
69
|
-
const item = await res.json();
|
|
70
|
-
expect(item.name).toBe("shield");
|
|
71
|
-
server.stop();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("unsupported method returns 405", async () => {
|
|
75
|
-
const server = await createServer({ port: 3004, routes: tmpDir });
|
|
76
|
-
const res = await fetch("http://localhost:3004/api/items", {
|
|
77
|
-
method: "DELETE",
|
|
78
|
-
});
|
|
79
|
-
expect(res.status).toBe(405);
|
|
80
|
-
server.stop();
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("Server — WebSocket routes", () => {
|
|
85
|
-
let wsDir: string;
|
|
86
|
-
let server: Awaited<ReturnType<typeof createServer>>;
|
|
87
|
-
|
|
88
|
-
beforeAll(async () => {
|
|
89
|
-
wsDir = join(tmpdir(), `grimoire-ws-${Date.now()}`);
|
|
90
|
-
await mkdir(join(wsDir, "echo"), { recursive: true });
|
|
91
|
-
await mkdir(join(wsDir, "params", "[docId]"), { recursive: true });
|
|
92
|
-
await mkdir(join(wsDir, "reject"), { recursive: true });
|
|
93
|
-
await mkdir(join(wsDir, "mixed"), { recursive: true });
|
|
94
|
-
|
|
95
|
-
// echo: upgrade() merges extra data, message handler echoes back
|
|
96
|
-
await writeFile(
|
|
97
|
-
join(wsDir, "echo", "+server.ts"),
|
|
98
|
-
`export function upgrade({ params }) {
|
|
99
|
-
return { sessionId: "abc123" };
|
|
100
|
-
}
|
|
101
|
-
export const websocket = {
|
|
102
|
-
message(ws, data) { ws.send("echo:" + data); },
|
|
103
|
-
};`,
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
// params: no upgrade(), ws.data.params still available
|
|
107
|
-
await writeFile(
|
|
108
|
-
join(wsDir, "params", "[docId]", "+server.ts"),
|
|
109
|
-
`export const websocket = {
|
|
110
|
-
open(ws) { ws.send("docId:" + ws.data.params.docId); },
|
|
111
|
-
};`,
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// reject: upgrade() throws → 426
|
|
115
|
-
await writeFile(
|
|
116
|
-
join(wsDir, "reject", "+server.ts"),
|
|
117
|
-
`export function upgrade() {
|
|
118
|
-
throw new Error("Unauthorized");
|
|
119
|
-
}
|
|
120
|
-
export const websocket = {
|
|
121
|
-
message(ws, data) { ws.send(data); },
|
|
122
|
-
};`,
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
// mixed: HTTP GET and WebSocket both exported
|
|
126
|
-
await writeFile(
|
|
127
|
-
join(wsDir, "mixed", "+server.ts"),
|
|
128
|
-
`export function GET() {
|
|
129
|
-
return Response.json({ kind: "http" });
|
|
130
|
-
}
|
|
131
|
-
export function upgrade({ params }) { return {}; }
|
|
132
|
-
export const websocket = {
|
|
133
|
-
message(ws, data) { ws.send(data); },
|
|
134
|
-
};`,
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
server = await createServer({ port: 3005, routes: wsDir });
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
afterAll(() => {
|
|
141
|
-
server.stop();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
function wsConnect(path: string): Promise<WebSocket> {
|
|
145
|
-
return new Promise((resolve, reject) => {
|
|
146
|
-
const ws = new WebSocket(`ws://localhost:3005${path}`);
|
|
147
|
-
ws.onopen = () => resolve(ws);
|
|
148
|
-
ws.onerror = () =>
|
|
149
|
-
reject(new Error(`WebSocket connection failed: ${path}`));
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function nextMessage(ws: WebSocket): Promise<string> {
|
|
154
|
-
return new Promise((resolve, reject) => {
|
|
155
|
-
const timer = setTimeout(
|
|
156
|
-
() => reject(new Error("timeout waiting for message")),
|
|
157
|
-
2000,
|
|
158
|
-
);
|
|
159
|
-
ws.onmessage = (ev) => {
|
|
160
|
-
clearTimeout(timer);
|
|
161
|
-
resolve(String(ev.data));
|
|
162
|
-
};
|
|
163
|
-
ws.onerror = () => {
|
|
164
|
-
clearTimeout(timer);
|
|
165
|
-
reject(new Error("ws error"));
|
|
166
|
-
};
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
test("message is echoed back", async () => {
|
|
171
|
-
const ws = await wsConnect("/echo");
|
|
172
|
-
const pending = nextMessage(ws);
|
|
173
|
-
ws.send("hello");
|
|
174
|
-
expect(await pending).toBe("echo:hello");
|
|
175
|
-
ws.close();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("upgrade() return value merges into ws.data", async () => {
|
|
179
|
-
// sessionId set in upgrade() — echo route sends it back via a second message
|
|
180
|
-
const ws = new WebSocket("ws://localhost:3005/echo");
|
|
181
|
-
// just verify connection opens cleanly (sessionId is in ws.data server-side)
|
|
182
|
-
await new Promise<void>((resolve, reject) => {
|
|
183
|
-
const t = setTimeout(() => reject(new Error("timeout")), 2000);
|
|
184
|
-
ws.onopen = () => {
|
|
185
|
-
clearTimeout(t);
|
|
186
|
-
resolve();
|
|
187
|
-
};
|
|
188
|
-
ws.onerror = () => {
|
|
189
|
-
clearTimeout(t);
|
|
190
|
-
reject(new Error("connect failed"));
|
|
191
|
-
};
|
|
192
|
-
});
|
|
193
|
-
ws.close();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("ws.data.params available when no upgrade() export", async () => {
|
|
197
|
-
const ws = await wsConnect("/params/42");
|
|
198
|
-
expect(await nextMessage(ws)).toBe("docId:42");
|
|
199
|
-
ws.close();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("upgrade() throwing rejects the connection", async () => {
|
|
203
|
-
await new Promise<void>((resolve, reject) => {
|
|
204
|
-
const ws = new WebSocket("ws://localhost:3005/reject");
|
|
205
|
-
const t = setTimeout(() => reject(new Error("timeout")), 2000);
|
|
206
|
-
ws.onopen = () => {
|
|
207
|
-
clearTimeout(t);
|
|
208
|
-
ws.close();
|
|
209
|
-
reject(new Error("should not have opened"));
|
|
210
|
-
};
|
|
211
|
-
ws.onerror = () => {
|
|
212
|
-
clearTimeout(t);
|
|
213
|
-
resolve();
|
|
214
|
-
};
|
|
215
|
-
ws.onclose = (ev) => {
|
|
216
|
-
clearTimeout(t);
|
|
217
|
-
if (ev.code !== 1006) resolve();
|
|
218
|
-
else resolve();
|
|
219
|
-
};
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test("plain HTTP GET on mixed route still works (no Upgrade header)", async () => {
|
|
224
|
-
const res = await fetch("http://localhost:3005/mixed");
|
|
225
|
-
expect(res.status).toBe(200);
|
|
226
|
-
const body = await res.json();
|
|
227
|
-
expect(body.kind).toBe("http");
|
|
228
|
-
});
|
|
229
|
-
});
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, rm, writeFile } from "fs/promises";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { createServer } from "../src/server";
|
|
6
|
+
|
|
7
|
+
// ── Sandbox note ─────────────────────────────────────────────────────────────
|
|
8
|
+
// The sigil babel plugin unconditionally inserts `import ... from "@sigil-dev/runtime"`
|
|
9
|
+
// for all non-SSR route files. When createServer() calls Bun.build to bundle hydrate.js,
|
|
10
|
+
// those compiled page files pull in @sigil-dev/runtime → Bun's test sandbox blocks the
|
|
11
|
+
// read of packages/runtime/index.ts.
|
|
12
|
+
//
|
|
13
|
+
// Consequence: createServer() integration tests can only use +server.ts API routes
|
|
14
|
+
// (which are never bundled by Bun.build). Form action behavior at the HTTP level cannot
|
|
15
|
+
// be tested here — it is covered at unit level by fail.test.ts and redirect-error.test.ts,
|
|
16
|
+
// and at the renderer level by rendering.test.ts.
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
let tmpDir: string;
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
tmpDir = join(tmpdir(), `grimoire-server-${Date.now()}`);
|
|
23
|
+
await mkdir(join(tmpDir, "api", "hello"), { recursive: true });
|
|
24
|
+
await mkdir(join(tmpDir, "api", "items"), { recursive: true });
|
|
25
|
+
|
|
26
|
+
await writeFile(
|
|
27
|
+
join(tmpDir, "api", "hello", "+server.ts"),
|
|
28
|
+
`export async function GET() { return Response.json({ message: "hello" }); }`,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
await writeFile(
|
|
32
|
+
join(tmpDir, "api", "items", "+server.ts"),
|
|
33
|
+
`export async function GET() {
|
|
34
|
+
return Response.json([{ id: 1, name: "sword" }]);
|
|
35
|
+
}
|
|
36
|
+
export async function POST({ request }) {
|
|
37
|
+
const body = await request.json();
|
|
38
|
+
return Response.json({ id: 2, ...body }, { status: 201 });
|
|
39
|
+
}`,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("Server — API routes (+server.ts)", () => {
|
|
44
|
+
test("GET returns JSON", async () => {
|
|
45
|
+
const server = await createServer({ port: 3004, routes: tmpDir });
|
|
46
|
+
const res = await fetch("http://localhost:3004/api/hello");
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
expect(data.message).toBe("hello");
|
|
49
|
+
server.stop();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("GET on multi-method route returns correct JSON", async () => {
|
|
53
|
+
const server = await createServer({ port: 3004, routes: tmpDir });
|
|
54
|
+
const res = await fetch("http://localhost:3004/api/items");
|
|
55
|
+
expect(res.status).toBe(200);
|
|
56
|
+
const items = await res.json();
|
|
57
|
+
expect(items[0].name).toBe("sword");
|
|
58
|
+
server.stop();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("POST returns 201 with created resource", async () => {
|
|
62
|
+
const server = await createServer({ port: 3004, routes: tmpDir });
|
|
63
|
+
const res = await fetch("http://localhost:3004/api/items", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ name: "shield" }),
|
|
67
|
+
});
|
|
68
|
+
expect(res.status).toBe(201);
|
|
69
|
+
const item = await res.json();
|
|
70
|
+
expect(item.name).toBe("shield");
|
|
71
|
+
server.stop();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("unsupported method returns 405", async () => {
|
|
75
|
+
const server = await createServer({ port: 3004, routes: tmpDir });
|
|
76
|
+
const res = await fetch("http://localhost:3004/api/items", {
|
|
77
|
+
method: "DELETE",
|
|
78
|
+
});
|
|
79
|
+
expect(res.status).toBe(405);
|
|
80
|
+
server.stop();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("Server — WebSocket routes", () => {
|
|
85
|
+
let wsDir: string;
|
|
86
|
+
let server: Awaited<ReturnType<typeof createServer>>;
|
|
87
|
+
|
|
88
|
+
beforeAll(async () => {
|
|
89
|
+
wsDir = join(tmpdir(), `grimoire-ws-${Date.now()}`);
|
|
90
|
+
await mkdir(join(wsDir, "echo"), { recursive: true });
|
|
91
|
+
await mkdir(join(wsDir, "params", "[docId]"), { recursive: true });
|
|
92
|
+
await mkdir(join(wsDir, "reject"), { recursive: true });
|
|
93
|
+
await mkdir(join(wsDir, "mixed"), { recursive: true });
|
|
94
|
+
|
|
95
|
+
// echo: upgrade() merges extra data, message handler echoes back
|
|
96
|
+
await writeFile(
|
|
97
|
+
join(wsDir, "echo", "+server.ts"),
|
|
98
|
+
`export function upgrade({ params }) {
|
|
99
|
+
return { sessionId: "abc123" };
|
|
100
|
+
}
|
|
101
|
+
export const websocket = {
|
|
102
|
+
message(ws, data) { ws.send("echo:" + data); },
|
|
103
|
+
};`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// params: no upgrade(), ws.data.params still available
|
|
107
|
+
await writeFile(
|
|
108
|
+
join(wsDir, "params", "[docId]", "+server.ts"),
|
|
109
|
+
`export const websocket = {
|
|
110
|
+
open(ws) { ws.send("docId:" + ws.data.params.docId); },
|
|
111
|
+
};`,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// reject: upgrade() throws → 426
|
|
115
|
+
await writeFile(
|
|
116
|
+
join(wsDir, "reject", "+server.ts"),
|
|
117
|
+
`export function upgrade() {
|
|
118
|
+
throw new Error("Unauthorized");
|
|
119
|
+
}
|
|
120
|
+
export const websocket = {
|
|
121
|
+
message(ws, data) { ws.send(data); },
|
|
122
|
+
};`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// mixed: HTTP GET and WebSocket both exported
|
|
126
|
+
await writeFile(
|
|
127
|
+
join(wsDir, "mixed", "+server.ts"),
|
|
128
|
+
`export function GET() {
|
|
129
|
+
return Response.json({ kind: "http" });
|
|
130
|
+
}
|
|
131
|
+
export function upgrade({ params }) { return {}; }
|
|
132
|
+
export const websocket = {
|
|
133
|
+
message(ws, data) { ws.send(data); },
|
|
134
|
+
};`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
server = await createServer({ port: 3005, routes: wsDir });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
afterAll(() => {
|
|
141
|
+
server.stop();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
function wsConnect(path: string): Promise<WebSocket> {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const ws = new WebSocket(`ws://localhost:3005${path}`);
|
|
147
|
+
ws.onopen = () => resolve(ws);
|
|
148
|
+
ws.onerror = () =>
|
|
149
|
+
reject(new Error(`WebSocket connection failed: ${path}`));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function nextMessage(ws: WebSocket): Promise<string> {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const timer = setTimeout(
|
|
156
|
+
() => reject(new Error("timeout waiting for message")),
|
|
157
|
+
2000,
|
|
158
|
+
);
|
|
159
|
+
ws.onmessage = (ev) => {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
resolve(String(ev.data));
|
|
162
|
+
};
|
|
163
|
+
ws.onerror = () => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
reject(new Error("ws error"));
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
test("message is echoed back", async () => {
|
|
171
|
+
const ws = await wsConnect("/echo");
|
|
172
|
+
const pending = nextMessage(ws);
|
|
173
|
+
ws.send("hello");
|
|
174
|
+
expect(await pending).toBe("echo:hello");
|
|
175
|
+
ws.close();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("upgrade() return value merges into ws.data", async () => {
|
|
179
|
+
// sessionId set in upgrade() — echo route sends it back via a second message
|
|
180
|
+
const ws = new WebSocket("ws://localhost:3005/echo");
|
|
181
|
+
// just verify connection opens cleanly (sessionId is in ws.data server-side)
|
|
182
|
+
await new Promise<void>((resolve, reject) => {
|
|
183
|
+
const t = setTimeout(() => reject(new Error("timeout")), 2000);
|
|
184
|
+
ws.onopen = () => {
|
|
185
|
+
clearTimeout(t);
|
|
186
|
+
resolve();
|
|
187
|
+
};
|
|
188
|
+
ws.onerror = () => {
|
|
189
|
+
clearTimeout(t);
|
|
190
|
+
reject(new Error("connect failed"));
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
ws.close();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("ws.data.params available when no upgrade() export", async () => {
|
|
197
|
+
const ws = await wsConnect("/params/42");
|
|
198
|
+
expect(await nextMessage(ws)).toBe("docId:42");
|
|
199
|
+
ws.close();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("upgrade() throwing rejects the connection", async () => {
|
|
203
|
+
await new Promise<void>((resolve, reject) => {
|
|
204
|
+
const ws = new WebSocket("ws://localhost:3005/reject");
|
|
205
|
+
const t = setTimeout(() => reject(new Error("timeout")), 2000);
|
|
206
|
+
ws.onopen = () => {
|
|
207
|
+
clearTimeout(t);
|
|
208
|
+
ws.close();
|
|
209
|
+
reject(new Error("should not have opened"));
|
|
210
|
+
};
|
|
211
|
+
ws.onerror = () => {
|
|
212
|
+
clearTimeout(t);
|
|
213
|
+
resolve();
|
|
214
|
+
};
|
|
215
|
+
ws.onclose = (ev) => {
|
|
216
|
+
clearTimeout(t);
|
|
217
|
+
if (ev.code !== 1006) resolve();
|
|
218
|
+
else resolve();
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("plain HTTP GET on mixed route still works (no Upgrade header)", async () => {
|
|
224
|
+
const res = await fetch("http://localhost:3005/mixed");
|
|
225
|
+
expect(res.status).toBe(200);
|
|
226
|
+
const body = await res.json();
|
|
227
|
+
expect(body.kind).toBe("http");
|
|
228
|
+
});
|
|
229
|
+
});
|