@mauroandre/velojs 0.0.22 → 0.0.24
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/README.md +55 -0
- package/dist/app-context.d.ts +38 -0
- package/dist/app-context.js +43 -0
- package/dist/events.d.ts +4 -0
- package/dist/events.js +124 -105
- package/dist/server.js +139 -139
- package/dist/testing/conventions.d.ts +25 -0
- package/dist/testing/createTestApp.d.ts +23 -0
- package/dist/testing/index.d.ts +20 -0
- package/dist/testing/index.js +488 -0
- package/dist/testing/internal.d.ts +30 -0
- package/dist/testing/mockContext.d.ts +10 -0
- package/dist/testing/subscription.d.ts +11 -0
- package/dist/testing/types.d.ts +125 -0
- package/package.json +5 -1
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { getRegisteredStreams as T } from "../events.js";
|
|
2
|
+
import { createIsolatedContext as E, withAppContext as R } from "../app-context.js";
|
|
3
|
+
function k(e) {
|
|
4
|
+
if (!e) return;
|
|
5
|
+
const r = Object.entries(e);
|
|
6
|
+
if (r.length !== 0)
|
|
7
|
+
return r.map(([a, u]) => `${encodeURIComponent(a)}=${encodeURIComponent(u)}`).join("; ");
|
|
8
|
+
}
|
|
9
|
+
function _(e) {
|
|
10
|
+
const r = {}, a = e.getSetCookie?.() ?? [];
|
|
11
|
+
for (const u of a) {
|
|
12
|
+
const i = u.indexOf(";"), l = i === -1 ? u : u.slice(0, i), h = l.indexOf("=");
|
|
13
|
+
if (h === -1) continue;
|
|
14
|
+
const c = l.slice(0, h).trim(), t = l.slice(h + 1).trim();
|
|
15
|
+
c && (r[c] = decodeURIComponent(t));
|
|
16
|
+
}
|
|
17
|
+
return r;
|
|
18
|
+
}
|
|
19
|
+
function x(e) {
|
|
20
|
+
if (!e) return "";
|
|
21
|
+
const r = new URLSearchParams();
|
|
22
|
+
for (const [u, i] of Object.entries(e))
|
|
23
|
+
if (Array.isArray(i))
|
|
24
|
+
for (const l of i) r.append(u, l);
|
|
25
|
+
else
|
|
26
|
+
r.set(u, i);
|
|
27
|
+
const a = r.toString();
|
|
28
|
+
return a ? `?${a}` : "";
|
|
29
|
+
}
|
|
30
|
+
function A(e) {
|
|
31
|
+
const r = {};
|
|
32
|
+
return e.forEach((a, u) => {
|
|
33
|
+
r[u] = a;
|
|
34
|
+
}), r;
|
|
35
|
+
}
|
|
36
|
+
function P(e) {
|
|
37
|
+
return e == null ? { body: null, contentType: null } : typeof e == "string" || e instanceof FormData || e instanceof URLSearchParams || e instanceof Blob || e instanceof ArrayBuffer || ArrayBuffer.isView(e) ? { body: e, contentType: null } : {
|
|
38
|
+
body: JSON.stringify(e),
|
|
39
|
+
contentType: "application/json"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const C = "http://velojs.test";
|
|
43
|
+
function S(e, r) {
|
|
44
|
+
const a = x(r);
|
|
45
|
+
return e.startsWith("http://") || e.startsWith("https://") ? e + a : (e.startsWith("/") || (e = "/" + e), C + e + a);
|
|
46
|
+
}
|
|
47
|
+
function I(e) {
|
|
48
|
+
const r = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map();
|
|
49
|
+
function u(i) {
|
|
50
|
+
for (const l of i) {
|
|
51
|
+
const h = l.module.metadata?.moduleId, c = l.module.metadata?.fullPath;
|
|
52
|
+
if (h) {
|
|
53
|
+
for (const o of Object.keys(l.module)) {
|
|
54
|
+
if (!o.startsWith("action_")) continue;
|
|
55
|
+
const f = l.module[o];
|
|
56
|
+
if (typeof f != "function") continue;
|
|
57
|
+
const d = o.replace("action_", "");
|
|
58
|
+
r.set(f, {
|
|
59
|
+
name: d,
|
|
60
|
+
fn: f,
|
|
61
|
+
moduleId: h,
|
|
62
|
+
url: `/_action/${h}/${d}`
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const t = l.module.loader;
|
|
66
|
+
if (typeof t == "function" && c) {
|
|
67
|
+
const o = a.get(t);
|
|
68
|
+
o ? o.paths.push(c) : a.set(t, {
|
|
69
|
+
fn: t,
|
|
70
|
+
moduleId: h,
|
|
71
|
+
paths: [c]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
l.children && u(l.children);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return u(e), { actions: r, loaders: a };
|
|
79
|
+
}
|
|
80
|
+
function q(e, r) {
|
|
81
|
+
if (typeof r == "string") return r;
|
|
82
|
+
const a = e.actions.get(r);
|
|
83
|
+
if (!a)
|
|
84
|
+
throw new Error(
|
|
85
|
+
`[velojs/testing] action_* function was not found in the route tree.
|
|
86
|
+
Make sure your Vitest config loads the VeloJS Vite plugin so that metadata is injected. See https://github.com/mauro-andre/velojs#testing`
|
|
87
|
+
);
|
|
88
|
+
return a.url;
|
|
89
|
+
}
|
|
90
|
+
function J(e, r, a) {
|
|
91
|
+
if (typeof r == "string") return r;
|
|
92
|
+
const u = e.loaders.get(r);
|
|
93
|
+
if (!u)
|
|
94
|
+
throw new Error(
|
|
95
|
+
`[velojs/testing] loader function was not found in the route tree.
|
|
96
|
+
Make sure your Vitest config loads the VeloJS Vite plugin so that metadata is injected.`
|
|
97
|
+
);
|
|
98
|
+
let i = u.paths[0];
|
|
99
|
+
if (a)
|
|
100
|
+
for (const [l, h] of Object.entries(a))
|
|
101
|
+
i = i.replace(`:${l}`, encodeURIComponent(h));
|
|
102
|
+
return i;
|
|
103
|
+
}
|
|
104
|
+
class $ {
|
|
105
|
+
buffer = "";
|
|
106
|
+
current = {
|
|
107
|
+
event: "message",
|
|
108
|
+
data: []
|
|
109
|
+
};
|
|
110
|
+
push(r) {
|
|
111
|
+
this.buffer += r;
|
|
112
|
+
const a = [];
|
|
113
|
+
let u;
|
|
114
|
+
for (; (u = this.buffer.indexOf(`
|
|
115
|
+
`)) !== -1; ) {
|
|
116
|
+
const i = this.buffer.slice(0, u);
|
|
117
|
+
if (this.buffer = this.buffer.slice(u + 1), i === "") {
|
|
118
|
+
if (this.current.data.length > 0 || this.current.event !== "message") {
|
|
119
|
+
const t = {
|
|
120
|
+
event: this.current.event,
|
|
121
|
+
data: this.current.data.join(`
|
|
122
|
+
`)
|
|
123
|
+
};
|
|
124
|
+
this.current.id !== void 0 && (t.id = this.current.id), a.push(t);
|
|
125
|
+
}
|
|
126
|
+
this.current = { event: "message", data: [] };
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (i.startsWith(":")) continue;
|
|
130
|
+
const l = i.indexOf(":");
|
|
131
|
+
if (l === -1) continue;
|
|
132
|
+
const h = i.slice(0, l);
|
|
133
|
+
let c = i.slice(l + 1);
|
|
134
|
+
c.startsWith(" ") && (c = c.slice(1)), h === "event" ? this.current.event = c : h === "data" ? this.current.data.push(c) : h === "id" && (this.current.id = c);
|
|
135
|
+
}
|
|
136
|
+
return a;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function H(e) {
|
|
140
|
+
const { response: r } = e, a = r.status, u = [];
|
|
141
|
+
let i = null, l = !1, h = null;
|
|
142
|
+
const c = [];
|
|
143
|
+
let t = null, o = Promise.resolve(void 0);
|
|
144
|
+
const f = (p) => {
|
|
145
|
+
for (; c.length > 0; ) {
|
|
146
|
+
const n = c.shift();
|
|
147
|
+
p ? n.reject(p) : n.reject(new Error("[velojs/testing] subscription closed before next event"));
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
if (a >= 200 && a < 300 && r.body) {
|
|
151
|
+
t = r.body.getReader();
|
|
152
|
+
const p = new TextDecoder(), n = new $();
|
|
153
|
+
o = (async () => {
|
|
154
|
+
try {
|
|
155
|
+
for (; ; ) {
|
|
156
|
+
const { value: s, done: w } = await t.read();
|
|
157
|
+
if (w) {
|
|
158
|
+
l || (l = !0), f();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const g = p.decode(s, { stream: !0 }), v = n.push(g);
|
|
162
|
+
for (const m of v) {
|
|
163
|
+
if (m.event === "snapshot") {
|
|
164
|
+
try {
|
|
165
|
+
i = JSON.parse(m.data);
|
|
166
|
+
} catch (b) {
|
|
167
|
+
h = b;
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (m.event === "close") {
|
|
172
|
+
l = !0, f(), t.cancel().catch(() => {
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (m.event !== "heartbeat")
|
|
177
|
+
try {
|
|
178
|
+
const b = JSON.parse(m.data);
|
|
179
|
+
u.push(b);
|
|
180
|
+
const j = c.shift();
|
|
181
|
+
j && j.resolve(b);
|
|
182
|
+
} catch (b) {
|
|
183
|
+
h = b;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (s) {
|
|
188
|
+
if (s?.name === "AbortError") {
|
|
189
|
+
f();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
f(s);
|
|
193
|
+
}
|
|
194
|
+
})();
|
|
195
|
+
} else
|
|
196
|
+
await r.text().catch(() => ""), l = !0;
|
|
197
|
+
const d = {
|
|
198
|
+
get status() {
|
|
199
|
+
return a;
|
|
200
|
+
},
|
|
201
|
+
get events() {
|
|
202
|
+
return u;
|
|
203
|
+
},
|
|
204
|
+
get snapshot() {
|
|
205
|
+
return i;
|
|
206
|
+
},
|
|
207
|
+
get closed() {
|
|
208
|
+
return l;
|
|
209
|
+
},
|
|
210
|
+
async next({ timeoutMs: p }) {
|
|
211
|
+
if (h) throw h;
|
|
212
|
+
if (u.length > y) {
|
|
213
|
+
const n = u[y];
|
|
214
|
+
return y++, n;
|
|
215
|
+
}
|
|
216
|
+
if (l)
|
|
217
|
+
throw new Error("[velojs/testing] subscription is closed");
|
|
218
|
+
return new Promise((n, s) => {
|
|
219
|
+
const w = setTimeout(() => {
|
|
220
|
+
const m = c.findIndex((b) => b.resolve === g);
|
|
221
|
+
m !== -1 && c.splice(m, 1), s(new Error(
|
|
222
|
+
`[velojs/testing] timed out after ${p}ms waiting for next event`
|
|
223
|
+
));
|
|
224
|
+
}, p), g = (m) => {
|
|
225
|
+
clearTimeout(w), y++, n(m);
|
|
226
|
+
}, v = (m) => {
|
|
227
|
+
clearTimeout(w), s(m);
|
|
228
|
+
};
|
|
229
|
+
c.push({ resolve: g, reject: v });
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
async nextN(p, { timeoutMs: n }) {
|
|
233
|
+
const s = Date.now(), w = [];
|
|
234
|
+
for (let g = 0; g < p; g++) {
|
|
235
|
+
const v = n - (Date.now() - s);
|
|
236
|
+
if (v <= 0)
|
|
237
|
+
throw new Error(
|
|
238
|
+
`[velojs/testing] timed out after ${n}ms waiting for ${p} events (got ${w.length})`
|
|
239
|
+
);
|
|
240
|
+
w.push(await d.next({ timeoutMs: v }));
|
|
241
|
+
}
|
|
242
|
+
return w;
|
|
243
|
+
},
|
|
244
|
+
async close() {
|
|
245
|
+
if (t)
|
|
246
|
+
try {
|
|
247
|
+
await t.cancel();
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
l = !0, f(), await o.catch(() => {
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
let y = 0;
|
|
255
|
+
return d;
|
|
256
|
+
}
|
|
257
|
+
function N(e = {}) {
|
|
258
|
+
const { user: r, params: a = {}, query: u = {}, body: i, headers: l = {}, cookies: h } = e, c = k(h), t = new Headers(l);
|
|
259
|
+
c && !t.has("cookie") && t.set("cookie", c);
|
|
260
|
+
const o = C + "/_mock" + x(u), f = {
|
|
261
|
+
method: i == null ? "GET" : "POST",
|
|
262
|
+
headers: t
|
|
263
|
+
};
|
|
264
|
+
i != null && (typeof i == "string" || i instanceof FormData || i instanceof Blob ? f.body = i : (f.body = JSON.stringify(i), t.has("content-type") || t.set("content-type", "application/json")));
|
|
265
|
+
const d = new Request(o, f), y = /* @__PURE__ */ new Map();
|
|
266
|
+
return r !== void 0 && y.set("user", r), {
|
|
267
|
+
req: {
|
|
268
|
+
raw: d,
|
|
269
|
+
url: d.url,
|
|
270
|
+
method: d.method,
|
|
271
|
+
param(n) {
|
|
272
|
+
return n === void 0 ? a : a[n];
|
|
273
|
+
},
|
|
274
|
+
query(n) {
|
|
275
|
+
if (n === void 0) {
|
|
276
|
+
const s = {};
|
|
277
|
+
for (const [w, g] of Object.entries(u))
|
|
278
|
+
s[w] = String(g);
|
|
279
|
+
return s;
|
|
280
|
+
}
|
|
281
|
+
return u[n];
|
|
282
|
+
},
|
|
283
|
+
queries(n) {
|
|
284
|
+
const s = u[n];
|
|
285
|
+
if (s !== void 0)
|
|
286
|
+
return Array.isArray(s) ? s : [s];
|
|
287
|
+
},
|
|
288
|
+
header(n) {
|
|
289
|
+
if (n === void 0) {
|
|
290
|
+
const s = {};
|
|
291
|
+
return t.forEach((w, g) => {
|
|
292
|
+
s[g] = w;
|
|
293
|
+
}), s;
|
|
294
|
+
}
|
|
295
|
+
return t.get(n) ?? void 0;
|
|
296
|
+
},
|
|
297
|
+
async json() {
|
|
298
|
+
return i == null ? null : typeof i == "object" ? i : JSON.parse(String(i));
|
|
299
|
+
},
|
|
300
|
+
async text() {
|
|
301
|
+
return i == null ? "" : typeof i == "string" ? i : JSON.stringify(i);
|
|
302
|
+
},
|
|
303
|
+
async parseBody() {
|
|
304
|
+
return i ?? {};
|
|
305
|
+
},
|
|
306
|
+
path: "/_mock"
|
|
307
|
+
},
|
|
308
|
+
res: void 0,
|
|
309
|
+
env: {},
|
|
310
|
+
finalized: !1,
|
|
311
|
+
get(n) {
|
|
312
|
+
return y.get(n);
|
|
313
|
+
},
|
|
314
|
+
set(n, s) {
|
|
315
|
+
y.set(n, s);
|
|
316
|
+
},
|
|
317
|
+
var: new Proxy({}, {
|
|
318
|
+
get(n, s) {
|
|
319
|
+
return y.get(s);
|
|
320
|
+
}
|
|
321
|
+
}),
|
|
322
|
+
json(n, s = 200) {
|
|
323
|
+
return new Response(JSON.stringify(n), {
|
|
324
|
+
status: s,
|
|
325
|
+
headers: { "content-type": "application/json" }
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
text(n, s = 200) {
|
|
329
|
+
return new Response(n, { status: s });
|
|
330
|
+
},
|
|
331
|
+
html(n, s = 200) {
|
|
332
|
+
return new Response(n, {
|
|
333
|
+
status: s,
|
|
334
|
+
headers: { "content-type": "text/html; charset=UTF-8" }
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
body(n, s = 200) {
|
|
338
|
+
return new Response(n, { status: s });
|
|
339
|
+
},
|
|
340
|
+
redirect(n, s = 302) {
|
|
341
|
+
return new Response(null, { status: s, headers: { location: n } });
|
|
342
|
+
},
|
|
343
|
+
header(n, s) {
|
|
344
|
+
},
|
|
345
|
+
status(n) {
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function W(e) {
|
|
350
|
+
const r = E();
|
|
351
|
+
return await R(r, async () => {
|
|
352
|
+
e.bootstrap && await e.bootstrap();
|
|
353
|
+
const { createApp: a } = await import("../server.js"), u = await a(e.routes), i = I(e.routes);
|
|
354
|
+
return V(u, r, i, e, void 0);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function V(e, r, a, u, i) {
|
|
358
|
+
async function l(t, o, f = {}) {
|
|
359
|
+
const d = S(o, f.query), y = new Headers(f.headers ?? {}), p = { ...f.cookies ?? {} }, n = k(p);
|
|
360
|
+
n && y.set("cookie", n);
|
|
361
|
+
let s = null;
|
|
362
|
+
if (f.body !== void 0) {
|
|
363
|
+
const m = P(f.body);
|
|
364
|
+
s = m.body, m.contentType && !y.has("content-type") && y.set("content-type", m.contentType);
|
|
365
|
+
}
|
|
366
|
+
const w = { method: t, headers: y };
|
|
367
|
+
s !== null && (w.body = s);
|
|
368
|
+
const g = new Request(d, w), v = await e.fetch(g);
|
|
369
|
+
return await B(v);
|
|
370
|
+
}
|
|
371
|
+
async function h(t, o, f = {}) {
|
|
372
|
+
const d = S(o, {
|
|
373
|
+
...f.query ?? {},
|
|
374
|
+
...f.channel ? { channel: f.channel } : {}
|
|
375
|
+
}), y = new Headers(f.headers ?? {}), p = { ...f.cookies ?? {} }, n = k(p);
|
|
376
|
+
n && y.set("cookie", n);
|
|
377
|
+
const s = new Request(d, { method: t, headers: y });
|
|
378
|
+
return await e.fetch(s);
|
|
379
|
+
}
|
|
380
|
+
const c = {
|
|
381
|
+
hono: e,
|
|
382
|
+
// HTTP
|
|
383
|
+
get: (t, o) => l("GET", t, o ?? {}),
|
|
384
|
+
post: (t, o) => l("POST", t, o ?? {}),
|
|
385
|
+
put: (t, o) => l("PUT", t, o ?? {}),
|
|
386
|
+
patch: (t, o) => l("PATCH", t, o ?? {}),
|
|
387
|
+
delete: (t, o) => l("DELETE", t, o ?? {}),
|
|
388
|
+
async action(t, o = {}) {
|
|
389
|
+
const f = q(a, t);
|
|
390
|
+
return await l("POST", f, {
|
|
391
|
+
...o,
|
|
392
|
+
body: o.body ?? {}
|
|
393
|
+
});
|
|
394
|
+
},
|
|
395
|
+
async loader(t, o = {}) {
|
|
396
|
+
const f = J(a, t, o.params), d = await l("GET", f, {
|
|
397
|
+
...o,
|
|
398
|
+
query: { ...o.query ?? {}, _data: "1" }
|
|
399
|
+
});
|
|
400
|
+
if (d.status >= 300 && d.status < 400 || d.status >= 400) return d;
|
|
401
|
+
const y = typeof t == "function" ? a.loaders.get(t)?.moduleId : void 0, p = await d.json();
|
|
402
|
+
return y && p && typeof p == "object" && y in p ? p[y] : p;
|
|
403
|
+
},
|
|
404
|
+
async subscribe(t, o = {}) {
|
|
405
|
+
const f = typeof t == "string" ? t : t.__path ?? D(), d = await h("GET", f, o);
|
|
406
|
+
return await H({ response: d });
|
|
407
|
+
},
|
|
408
|
+
async sessionCookies({ user: t }) {
|
|
409
|
+
if (!u.getSessionCookie)
|
|
410
|
+
throw new Error(
|
|
411
|
+
"[velojs/testing] sessionCookies/as requires `getSessionCookie` in createTestApp options."
|
|
412
|
+
);
|
|
413
|
+
return await u.getSessionCookie({ user: t });
|
|
414
|
+
},
|
|
415
|
+
as({ user: t }) {
|
|
416
|
+
if (!u.getSessionCookie)
|
|
417
|
+
throw new Error(
|
|
418
|
+
"[velojs/testing] app.as() requires `getSessionCookie` in createTestApp options."
|
|
419
|
+
);
|
|
420
|
+
let o = null;
|
|
421
|
+
const f = () => (o || (o = Promise.resolve(u.getSessionCookie({ user: t }))), o), d = (p) => (async (...n) => {
|
|
422
|
+
const s = await f(), [w, g] = n, v = {
|
|
423
|
+
...g ?? {},
|
|
424
|
+
cookies: { ...s, ...g?.cookies ?? {} }
|
|
425
|
+
};
|
|
426
|
+
return await p.call(c, w, v);
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
hono: e,
|
|
430
|
+
get: d(c.get),
|
|
431
|
+
post: d(c.post),
|
|
432
|
+
put: d(c.put),
|
|
433
|
+
patch: d(c.patch),
|
|
434
|
+
delete: d(c.delete),
|
|
435
|
+
action: d(c.action),
|
|
436
|
+
loader: d(c.loader),
|
|
437
|
+
subscribe: d(c.subscribe),
|
|
438
|
+
sessionCookies: c.sessionCookies,
|
|
439
|
+
as: c.as,
|
|
440
|
+
// chaining .as.as works — last wins
|
|
441
|
+
mockContext: c.mockContext,
|
|
442
|
+
reset: c.reset,
|
|
443
|
+
close: c.close
|
|
444
|
+
};
|
|
445
|
+
},
|
|
446
|
+
mockContext(t = {}) {
|
|
447
|
+
return N(t);
|
|
448
|
+
},
|
|
449
|
+
async reset() {
|
|
450
|
+
for (const t of T())
|
|
451
|
+
try {
|
|
452
|
+
t.__reset();
|
|
453
|
+
} catch (o) {
|
|
454
|
+
console.error("[velojs/testing] stream.__reset failed:", o);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
async close() {
|
|
458
|
+
await c.reset();
|
|
459
|
+
for (const t of r.disposers)
|
|
460
|
+
try {
|
|
461
|
+
t();
|
|
462
|
+
} catch (o) {
|
|
463
|
+
console.error("[velojs/testing] disposer failed:", o);
|
|
464
|
+
}
|
|
465
|
+
r.disposers.clear();
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
return c;
|
|
469
|
+
}
|
|
470
|
+
async function B(e) {
|
|
471
|
+
return {
|
|
472
|
+
status: e.status,
|
|
473
|
+
headers: A(e.headers),
|
|
474
|
+
cookies: _(e.headers),
|
|
475
|
+
json: () => e.clone().json(),
|
|
476
|
+
text: () => e.clone().text(),
|
|
477
|
+
blob: () => e.clone().blob(),
|
|
478
|
+
raw: e
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function D(e) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
"[velojs/testing] subscribe(stream) — stream has no __path. If this is a stream_* declaration, make sure your routes import is processed by the VeloJS Vite plugin (your vitest.config.ts must include `veloPlugin()`)."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
export {
|
|
487
|
+
W as createTestApp
|
|
488
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers — not exported from `@mauroandre/velojs/testing`.
|
|
3
|
+
*/
|
|
4
|
+
import type { Cookies, Headers as TestHeaders, Query } from "./types.js";
|
|
5
|
+
/** Serialize cookies object into a `Cookie` header value. */
|
|
6
|
+
export declare function serializeCookies(cookies?: Cookies): string | undefined;
|
|
7
|
+
/** Parse Set-Cookie header(s) into a cookies record (best effort). */
|
|
8
|
+
export declare function parseSetCookies(headers: Headers): Cookies;
|
|
9
|
+
/** Convert query object to URL search string (without leading `?`). */
|
|
10
|
+
export declare function buildQueryString(query?: Query): string;
|
|
11
|
+
/** Convert headers record (any case) to a Headers instance. */
|
|
12
|
+
export declare function toHeaders(record?: TestHeaders): Headers;
|
|
13
|
+
/** Headers instance → flat record. */
|
|
14
|
+
export declare function fromHeaders(h: Headers): TestHeaders;
|
|
15
|
+
/**
|
|
16
|
+
* Encode a body for fetch:
|
|
17
|
+
* - FormData / URLSearchParams / Blob / ReadableStream / string → passed as-is
|
|
18
|
+
* - undefined/null → no body
|
|
19
|
+
* - everything else → JSON.stringify + Content-Type: application/json
|
|
20
|
+
*/
|
|
21
|
+
export declare function encodeBody(body: unknown): {
|
|
22
|
+
body: BodyInit | null;
|
|
23
|
+
contentType: string | null;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Build a Request URL using a base — Hono's `app.fetch` requires absolute URLs.
|
|
27
|
+
* We use a fixed in-memory host since no socket is opened.
|
|
28
|
+
*/
|
|
29
|
+
export declare const TEST_BASE_URL = "http://velojs.test";
|
|
30
|
+
export declare function buildUrl(path: string, query?: Query): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape hatch — build a partial Hono Context for direct invocation of
|
|
3
|
+
* actions / loaders / jobs without going through HTTP.
|
|
4
|
+
*
|
|
5
|
+
* Middlewares do NOT run. This is intentional — pass `c.set("user", ...)` etc
|
|
6
|
+
* via the `user` option if your handler reads from `c.get("user")`.
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from "hono";
|
|
9
|
+
import type { MockContextOptions } from "./types.js";
|
|
10
|
+
export declare function buildMockContext(opts?: MockContextOptions): Context;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE subscription helper — parses the SSE protocol from a streaming Response
|
|
3
|
+
* and exposes assertion-friendly methods for tests.
|
|
4
|
+
*/
|
|
5
|
+
import type { TestSubscription } from "./types.js";
|
|
6
|
+
interface BuildSubscriptionOptions {
|
|
7
|
+
response: Response;
|
|
8
|
+
parseJson?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function buildSubscription<TEvent, TSnapshot>(opts: BuildSubscriptionOptions): Promise<TestSubscription<TEvent, TSnapshot>>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the VeloJS testing toolkit.
|
|
3
|
+
*/
|
|
4
|
+
import type { Hono, Context } from "hono";
|
|
5
|
+
import type { AppRoutes } from "../types.js";
|
|
6
|
+
import type { EventStream } from "../events.js";
|
|
7
|
+
export type Cookies = Record<string, string>;
|
|
8
|
+
export type Headers = Record<string, string>;
|
|
9
|
+
export type Query = Record<string, string | string[]>;
|
|
10
|
+
export type Params = Record<string, string>;
|
|
11
|
+
export interface CreateTestAppOptions {
|
|
12
|
+
/** App routes — typically `import { routes } from "./app/routes.js"`. */
|
|
13
|
+
routes: AppRoutes;
|
|
14
|
+
/**
|
|
15
|
+
* Optional setup that runs once before the app is created.
|
|
16
|
+
* Use for DB connections, index creation, or any side effects that the
|
|
17
|
+
* production `app/server.tsx` would do at startup.
|
|
18
|
+
*
|
|
19
|
+
* `addRoutes()` and `onServer()` calls here are scoped to this app only.
|
|
20
|
+
*/
|
|
21
|
+
bootstrap?: () => void | Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Optional callback that maps a `user` (or any opaque object) to the
|
|
24
|
+
* cookies the test client should attach. Required for `app.as(user)` and
|
|
25
|
+
* `app.sessionCookies(user)`.
|
|
26
|
+
*/
|
|
27
|
+
getSessionCookie?: (input: {
|
|
28
|
+
user: any;
|
|
29
|
+
}) => Promise<Cookies> | Cookies;
|
|
30
|
+
}
|
|
31
|
+
export interface RequestOptions {
|
|
32
|
+
/** Cookies serialized into the `Cookie` header. */
|
|
33
|
+
cookies?: Cookies;
|
|
34
|
+
/** Extra headers (case-insensitive). */
|
|
35
|
+
headers?: Headers;
|
|
36
|
+
/** Query string. Arrays become repeated keys: `{ tag: ["a","b"] }` → `?tag=a&tag=b`. */
|
|
37
|
+
query?: Query;
|
|
38
|
+
}
|
|
39
|
+
export interface BodyRequestOptions extends RequestOptions {
|
|
40
|
+
/** Body — plain object → JSON; FormData/URLSearchParams/Blob/string passed as-is. */
|
|
41
|
+
body?: unknown;
|
|
42
|
+
}
|
|
43
|
+
export interface LoaderRequestOptions extends RequestOptions {
|
|
44
|
+
/** URL params (for routes with `:id` placeholders). */
|
|
45
|
+
params?: Params;
|
|
46
|
+
}
|
|
47
|
+
/** Wraps the Hono Response. */
|
|
48
|
+
export interface TestResponse {
|
|
49
|
+
status: number;
|
|
50
|
+
headers: Headers;
|
|
51
|
+
json<T = any>(): Promise<T>;
|
|
52
|
+
text(): Promise<string>;
|
|
53
|
+
blob(): Promise<Blob>;
|
|
54
|
+
/** Cookie header parsed: `{ session: "value" }`. */
|
|
55
|
+
cookies: Cookies;
|
|
56
|
+
/** Underlying Response (Fetch spec). */
|
|
57
|
+
raw: Response;
|
|
58
|
+
}
|
|
59
|
+
export interface SubscribeOptions extends RequestOptions {
|
|
60
|
+
/** Channel ID — sent as `?channel=...`. */
|
|
61
|
+
channel?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface NextOptions {
|
|
64
|
+
/** Reject if no event arrives within this many ms. */
|
|
65
|
+
timeoutMs: number;
|
|
66
|
+
}
|
|
67
|
+
export interface TestSubscription<TEvent = any, TSnapshot = any> {
|
|
68
|
+
/** HTTP status of the initial SSE response (200, 403, etc). */
|
|
69
|
+
readonly status: number;
|
|
70
|
+
/** All events received since connect, in order. */
|
|
71
|
+
readonly events: ReadonlyArray<TEvent>;
|
|
72
|
+
/** Snapshot from the server (if any). */
|
|
73
|
+
readonly snapshot: TSnapshot | null;
|
|
74
|
+
/** True when the server signaled a deliberate close (closeOn / stream.close). */
|
|
75
|
+
readonly closed: boolean;
|
|
76
|
+
/** Wait for the next event. Rejects on timeout. */
|
|
77
|
+
next(opts: NextOptions): Promise<TEvent>;
|
|
78
|
+
/** Wait for N events. Total timeout, not per-event. */
|
|
79
|
+
nextN(n: number, opts: NextOptions): Promise<TEvent[]>;
|
|
80
|
+
/** Force disconnect. Triggers per-channel source abortSignal server-side. */
|
|
81
|
+
close(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
export interface MockContextOptions {
|
|
84
|
+
user?: any;
|
|
85
|
+
params?: Params;
|
|
86
|
+
query?: Record<string, string>;
|
|
87
|
+
body?: unknown;
|
|
88
|
+
headers?: Headers;
|
|
89
|
+
cookies?: Cookies;
|
|
90
|
+
}
|
|
91
|
+
export interface TestApp {
|
|
92
|
+
/** Underlying Hono app. */
|
|
93
|
+
readonly hono: Hono;
|
|
94
|
+
get(path: string, opts?: RequestOptions): Promise<TestResponse>;
|
|
95
|
+
post(path: string, opts?: BodyRequestOptions): Promise<TestResponse>;
|
|
96
|
+
put(path: string, opts?: BodyRequestOptions): Promise<TestResponse>;
|
|
97
|
+
patch(path: string, opts?: BodyRequestOptions): Promise<TestResponse>;
|
|
98
|
+
delete(path: string, opts?: RequestOptions): Promise<TestResponse>;
|
|
99
|
+
/**
|
|
100
|
+
* Invoke a `action_*` server function via HTTP. Auto-resolves URL from the
|
|
101
|
+
* function's metadata (injected by the VeloJS Vite plugin).
|
|
102
|
+
*/
|
|
103
|
+
action(fn: Function | string, opts?: BodyRequestOptions): Promise<TestResponse>;
|
|
104
|
+
/**
|
|
105
|
+
* Invoke a `loader` server function. By default unwraps the response data:
|
|
106
|
+
* - Resolved normally → returns the loader's return value
|
|
107
|
+
* - Threw → re-throws
|
|
108
|
+
* - Redirected → returns a `TestResponse` for status inspection
|
|
109
|
+
*/
|
|
110
|
+
loader<T = any>(fn: Function | string, opts?: LoaderRequestOptions): Promise<T | TestResponse>;
|
|
111
|
+
subscribe<TEvent = any, TSnapshot = any>(stream: EventStream<TEvent, TSnapshot> | string, opts?: SubscribeOptions): Promise<TestSubscription<TEvent, TSnapshot>>;
|
|
112
|
+
/** Build cookies for a user via `getSessionCookie` config. */
|
|
113
|
+
sessionCookies(input: {
|
|
114
|
+
user: any;
|
|
115
|
+
}): Promise<Cookies>;
|
|
116
|
+
/** Sub-client with cookies automatically applied to every request. */
|
|
117
|
+
as(input: {
|
|
118
|
+
user: any;
|
|
119
|
+
}): TestApp;
|
|
120
|
+
mockContext(opts?: MockContextOptions): Context;
|
|
121
|
+
/** Reset all transient state (stream buffers, listeners, retention timers). */
|
|
122
|
+
reset(): Promise<void>;
|
|
123
|
+
/** Tear down everything (timers, callbacks). Vitest must report zero open handles. */
|
|
124
|
+
close(): Promise<void>;
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mauroandre/velojs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fullstack web framework built on Hono + Preact with Vite-powered AST transforms",
|
|
6
6
|
"keywords": [
|
|
@@ -57,6 +57,10 @@
|
|
|
57
57
|
"types": "./dist/events.d.ts",
|
|
58
58
|
"import": "./dist/events.js"
|
|
59
59
|
},
|
|
60
|
+
"./testing": {
|
|
61
|
+
"types": "./dist/testing/index.d.ts",
|
|
62
|
+
"import": "./dist/testing/index.js"
|
|
63
|
+
},
|
|
60
64
|
"./cookie": {
|
|
61
65
|
"types": "./dist/cookie.d.ts",
|
|
62
66
|
"import": "./dist/cookie.js"
|