1tube 0.2.0 → 0.3.0

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.
@@ -0,0 +1,464 @@
1
+ /**
2
+ * Runtime-agnostic assertions for `1tube/test`.
3
+ *
4
+ * Pure TypeScript with no runtime imports, so the same assertions run under
5
+ * `bun test`, `node --test`, or a workerd test pool. The surface follows the
6
+ * familiar `assertX(actual, expected, msg?)` shape (the one Supabase-style
7
+ * edge function tests are written against), so test files only change their
8
+ * import line when moving between runners.
9
+ */
10
+
11
+ export class AssertionError extends Error {
12
+ override name = "AssertionError";
13
+ constructor(message: string) {
14
+ super(message);
15
+ }
16
+ }
17
+
18
+ type AnyConstructor = abstract new (...args: never[]) => unknown;
19
+ type ErrorClass<E extends Error = Error> = abstract new (...args: never[]) => E;
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Formatting
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** Readable, deterministic rendering of a value for failure messages. */
26
+ export function format(value: unknown): string {
27
+ const seen = new WeakSet<object>();
28
+ const indent = (s: string) => s.replace(/\n/g, "\n ");
29
+ const render = (v: unknown): string => {
30
+ switch (typeof v) {
31
+ case "string":
32
+ return JSON.stringify(v);
33
+ case "bigint":
34
+ return `${v}n`;
35
+ case "symbol":
36
+ case "undefined":
37
+ case "boolean":
38
+ return String(v);
39
+ case "number":
40
+ return Object.is(v, -0) ? "-0" : String(v);
41
+ case "function":
42
+ return `[Function: ${(v as { name?: string }).name || "anonymous"}]`;
43
+ }
44
+ if (v === null) return "null";
45
+ const obj = v as object;
46
+ if (seen.has(obj)) return "[Circular]";
47
+ if (obj instanceof Date) return `Date(${Number.isNaN(obj.getTime()) ? "Invalid" : obj.toISOString()})`;
48
+ if (obj instanceof RegExp) return String(obj);
49
+ if (obj instanceof Error) return `${obj.name}: ${obj.message}`;
50
+ if (typeof URL !== "undefined" && obj instanceof URL) return `URL(${obj.href})`;
51
+ seen.add(obj);
52
+ try {
53
+ if (Array.isArray(obj)) {
54
+ if (obj.length === 0) return "[]";
55
+ return `[\n ${obj.map((x) => indent(render(x))).join(",\n ")},\n]`;
56
+ }
57
+ if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
58
+ const items = Array.from(obj as unknown as ArrayLike<number | bigint>, (x) => render(x));
59
+ return `${obj.constructor.name}(${items.length}) [${items.join(", ")}]`;
60
+ }
61
+ if (obj instanceof Map) {
62
+ if (obj.size === 0) return "Map(0) {}";
63
+ const rows = [...obj].map(([k, x]) => `${indent(render(k))} => ${indent(render(x))}`);
64
+ return `Map(${obj.size}) {\n ${rows.join(",\n ")},\n}`;
65
+ }
66
+ if (obj instanceof Set) {
67
+ if (obj.size === 0) return "Set(0) {}";
68
+ return `Set(${obj.size}) {\n ${[...obj].map((x) => indent(render(x))).join(",\n ")},\n}`;
69
+ }
70
+ const keys: (string | symbol)[] = [
71
+ ...Object.keys(obj),
72
+ ...Object.getOwnPropertySymbols(obj).filter((s) =>
73
+ Object.prototype.propertyIsEnumerable.call(obj, s)
74
+ ),
75
+ ];
76
+ const proto = Object.getPrototypeOf(obj);
77
+ const tag = proto === null
78
+ ? "[Object: null prototype] "
79
+ : proto !== Object.prototype && proto?.constructor?.name
80
+ ? `${proto.constructor.name} `
81
+ : "";
82
+ if (keys.length === 0) return `${tag}{}`;
83
+ const rows = keys.map((k) => {
84
+ const name = typeof k === "symbol"
85
+ ? `[${String(k)}]`
86
+ : /^[A-Za-z_$][\w$]*$/.test(k)
87
+ ? k
88
+ : JSON.stringify(k);
89
+ return `${name}: ${indent(render((obj as Record<string | symbol, unknown>)[k]))}`;
90
+ });
91
+ return `${tag}{\n ${rows.join(",\n ")},\n}`;
92
+ } finally {
93
+ seen.delete(obj);
94
+ }
95
+ };
96
+ return render(value);
97
+ }
98
+
99
+ /** Line-level diff of two formatted values (`-` actual, `+` expected). */
100
+ function diff(actual: unknown, expected: unknown): string {
101
+ const a = format(actual).split("\n");
102
+ const b = format(expected).split("\n");
103
+ // LCS over lines: values printed for assertion failures are small.
104
+ const lcs: number[][] = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
105
+ for (let i = a.length - 1; i >= 0; i--) {
106
+ for (let j = b.length - 1; j >= 0; j--) {
107
+ lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
108
+ }
109
+ }
110
+ const out: string[] = [];
111
+ let i = 0;
112
+ let j = 0;
113
+ while (i < a.length || j < b.length) {
114
+ if (i < a.length && j < b.length && a[i] === b[j]) {
115
+ out.push(` ${a[i]}`);
116
+ i++;
117
+ j++;
118
+ } else if (j < b.length && (i >= a.length || lcs[i][j + 1] >= lcs[i + 1][j])) {
119
+ out.push(`+ ${b[j++]}`);
120
+ } else {
121
+ out.push(`- ${a[i++]}`);
122
+ }
123
+ }
124
+ return out.join("\n");
125
+ }
126
+
127
+ const suffix = (msg?: string) => (msg ? `: ${msg}` : "");
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Equality
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /**
134
+ * Deep structural equality: same constructor, same own enumerable keys
135
+ * (strings and symbols), recursively equal values. `NaN` equals `NaN`;
136
+ * `0` and `-0` differ. Maps and Sets compare by content, order-insensitive.
137
+ */
138
+ export function equal(a: unknown, b: unknown): boolean {
139
+ const seen = new Map<object, object>();
140
+ const eq = (x: unknown, y: unknown): boolean => {
141
+ if (Object.is(x, y)) return true;
142
+ if (typeof x !== "object" || typeof y !== "object" || x === null || y === null) {
143
+ return false;
144
+ }
145
+ if (seen.get(x) === y) return true;
146
+ if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) return false;
147
+ seen.set(x, y);
148
+
149
+ if (x instanceof Date) return Object.is(x.getTime(), (y as Date).getTime());
150
+ if (x instanceof RegExp) return String(x) === String(y);
151
+ if (typeof URL !== "undefined" && x instanceof URL) return x.href === (y as URL).href;
152
+ if (x instanceof WeakMap || x instanceof WeakSet || x instanceof WeakRef) {
153
+ throw new TypeError("cannot compare WeakMap, WeakSet or WeakRef instances");
154
+ }
155
+ if (x instanceof Number || x instanceof String || x instanceof Boolean) {
156
+ if (!Object.is(x.valueOf(), (y as typeof x).valueOf())) return false;
157
+ }
158
+ if (x instanceof ArrayBuffer) {
159
+ return eq(new Uint8Array(x), new Uint8Array(y as ArrayBuffer));
160
+ }
161
+ if (x instanceof DataView) {
162
+ return eq(
163
+ new Uint8Array(x.buffer, x.byteOffset, x.byteLength),
164
+ new Uint8Array((y as DataView).buffer, (y as DataView).byteOffset, (y as DataView).byteLength),
165
+ );
166
+ }
167
+ if (ArrayBuffer.isView(x)) {
168
+ const xa = x as unknown as ArrayLike<unknown>;
169
+ const ya = y as unknown as ArrayLike<unknown>;
170
+ if (xa.length !== ya.length) return false;
171
+ for (let i = 0; i < xa.length; i++) if (!Object.is(xa[i], ya[i])) return false;
172
+ return true;
173
+ }
174
+ if (x instanceof Map) {
175
+ const ym = y as Map<unknown, unknown>;
176
+ if (x.size !== ym.size) return false;
177
+ const unmatched = [...ym];
178
+ for (const [k, v] of x) {
179
+ const idx = unmatched.findIndex(([k2, v2]) => eq(k, k2) && eq(v, v2));
180
+ if (idx === -1) return false;
181
+ unmatched.splice(idx, 1);
182
+ }
183
+ return true;
184
+ }
185
+ if (x instanceof Set) {
186
+ const ys = y as Set<unknown>;
187
+ if (x.size !== ys.size) return false;
188
+ const unmatched = [...ys];
189
+ for (const v of x) {
190
+ const idx = unmatched.findIndex((v2) => eq(v, v2));
191
+ if (idx === -1) return false;
192
+ unmatched.splice(idx, 1);
193
+ }
194
+ return true;
195
+ }
196
+ if (x instanceof Error) {
197
+ const ye = y as Error;
198
+ if (x.name !== ye.name || x.message !== ye.message) return false;
199
+ }
200
+
201
+ const keysOf = (o: object) => [
202
+ ...Object.keys(o),
203
+ ...Object.getOwnPropertySymbols(o).filter((s) => Object.prototype.propertyIsEnumerable.call(o, s)),
204
+ ];
205
+ const xk = keysOf(x);
206
+ const yk = keysOf(y);
207
+ if (xk.length !== yk.length) return false;
208
+ const rec = (o: object, k: string | symbol) => (o as Record<string | symbol, unknown>)[k];
209
+ for (const k of xk) {
210
+ if (!Object.prototype.hasOwnProperty.call(y, k)) return false;
211
+ if (!eq(rec(x, k), rec(y, k))) return false;
212
+ }
213
+ return true;
214
+ };
215
+ return eq(a, b);
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Assertions
220
+ // ---------------------------------------------------------------------------
221
+
222
+ /** Assert that `expr` is truthy. */
223
+ export function assert(expr: unknown, msg = ""): asserts expr {
224
+ if (!expr) throw new AssertionError(msg || "Expected value to be truthy");
225
+ }
226
+
227
+ /** Assert that `expr` is falsy. */
228
+ export function assertFalse(expr: unknown, msg = ""): void {
229
+ if (expr) throw new AssertionError(msg || `Expected value to be falsy, got ${format(expr)}`);
230
+ }
231
+
232
+ /** Deep equality (see {@link equal}). */
233
+ export function assertEquals<T>(actual: T, expected: T, msg?: string): void {
234
+ if (equal(actual, expected)) return;
235
+ throw new AssertionError(
236
+ `Values are not equal${suffix(msg)}\n\n [Diff] Actual / Expected\n\n${diff(actual, expected)}\n`,
237
+ );
238
+ }
239
+
240
+ export function assertNotEquals<T>(actual: T, expected: T, msg?: string): void {
241
+ if (!equal(actual, expected)) return;
242
+ throw new AssertionError(`Expected actual: ${format(actual)} not to be: ${format(expected)}${suffix(msg)}`);
243
+ }
244
+
245
+ /** Identity (`Object.is`). */
246
+ export function assertStrictEquals<T>(actual: unknown, expected: T, msg?: string): asserts actual is T {
247
+ if (Object.is(actual, expected)) return;
248
+ const sameRendering = format(actual) === format(expected);
249
+ throw new AssertionError(
250
+ sameRendering
251
+ ? `Values have the same structure but are not reference-equal${suffix(msg)}\n\n ${format(actual).replace(/\n/g, "\n ")}\n`
252
+ : `Values are not strictly equal${suffix(msg)}\n\n [Diff] Actual / Expected\n\n${diff(actual, expected)}\n`,
253
+ );
254
+ }
255
+
256
+ export function assertNotStrictEquals<T>(actual: T, expected: T, msg?: string): void {
257
+ if (!Object.is(actual, expected)) return;
258
+ throw new AssertionError(`Expected "actual" to not be strictly equal to: ${format(expected)}${suffix(msg)}`);
259
+ }
260
+
261
+ /** |actual - expected| <= tolerance (default 1e-7). */
262
+ export function assertAlmostEquals(actual: number, expected: number, tolerance = 1e-7, msg?: string): void {
263
+ if (Object.is(actual, expected)) return;
264
+ const delta = Math.abs(expected - actual);
265
+ if (delta <= tolerance) return;
266
+ throw new AssertionError(
267
+ `Expected actual: "${actual}" to be close to "${expected}": delta "${delta}" is greater than "${tolerance}"${suffix(msg)}`,
268
+ );
269
+ }
270
+
271
+ /** Not `null` and not `undefined`. */
272
+ export function assertExists<T>(actual: T, msg?: string): asserts actual is NonNullable<T> {
273
+ if (actual === undefined || actual === null) {
274
+ throw new AssertionError(msg || `Expected actual: "${actual}" to not be null or undefined`);
275
+ }
276
+ }
277
+
278
+ export function assertInstanceOf<T extends AnyConstructor>(
279
+ actual: unknown,
280
+ expectedType: T,
281
+ msg?: string,
282
+ ): asserts actual is InstanceType<T> {
283
+ if (actual instanceof expectedType) return;
284
+ const got = actual === null
285
+ ? "null"
286
+ : typeof actual === "object"
287
+ ? (Object.getPrototypeOf(actual)?.constructor?.name ?? "Object")
288
+ : typeof actual;
289
+ throw new AssertionError(
290
+ `Expected object to be an instance of "${expectedType.name}" but was "${got}"${suffix(msg)}`,
291
+ );
292
+ }
293
+
294
+ export function assertNotInstanceOf<T extends AnyConstructor>(actual: unknown, unexpectedType: T, msg?: string): void {
295
+ if (!(actual instanceof unexpectedType)) return;
296
+ throw new AssertionError(`Expected object to not be an instance of "${unexpectedType.name}"${suffix(msg)}`);
297
+ }
298
+
299
+ export function assertStringIncludes(actual: string, expected: string, msg?: string): void {
300
+ if (typeof actual === "string" && actual.includes(expected)) return;
301
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to contain: ${format(expected)}`);
302
+ }
303
+
304
+ export function assertMatch(actual: string, expected: RegExp, msg?: string): void {
305
+ expected.lastIndex = 0;
306
+ if (expected.test(actual)) return;
307
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to match: ${expected}`);
308
+ }
309
+
310
+ export function assertNotMatch(actual: string, expected: RegExp, msg?: string): void {
311
+ expected.lastIndex = 0;
312
+ if (!expected.test(actual)) return;
313
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to not match: ${expected}`);
314
+ }
315
+
316
+ /** Every element of `expected` is deep-equal to some element of `actual`. */
317
+ export function assertArrayIncludes<T>(actual: ArrayLike<T>, expected: ArrayLike<T>, msg?: string): void {
318
+ const missing: T[] = [];
319
+ for (const e of Array.from(expected)) {
320
+ if (!Array.from(actual).some((a) => equal(a, e))) missing.push(e);
321
+ }
322
+ if (missing.length === 0) return;
323
+ throw new AssertionError(
324
+ msg || `Expected actual: ${format(actual)} to include: ${format(expected)}\nmissing: ${format(missing)}`,
325
+ );
326
+ }
327
+
328
+ /** `actual` contains (recursively) every key/value of `expected`. */
329
+ export function assertObjectMatch(actual: Record<PropertyKey, unknown>, expected: Record<PropertyKey, unknown>, msg?: string): void {
330
+ const pick = (a: unknown, e: unknown): unknown => {
331
+ if (Array.isArray(e) && Array.isArray(a)) return a.map((x, i) => (i < e.length ? pick(x, e[i]) : x));
332
+ if (e && typeof e === "object" && a && typeof a === "object" && !(e instanceof Date) && !(e instanceof RegExp)) {
333
+ const out: Record<PropertyKey, unknown> = {};
334
+ for (const k of Reflect.ownKeys(e)) {
335
+ if (k in (a as object)) out[k] = pick((a as Record<PropertyKey, unknown>)[k], (e as Record<PropertyKey, unknown>)[k]);
336
+ }
337
+ return out;
338
+ }
339
+ return a;
340
+ };
341
+ assertEquals(pick(actual, expected), expected as unknown, msg);
342
+ }
343
+
344
+ export function assertGreater<T>(actual: T, expected: T, msg?: string): void {
345
+ if (actual > expected) return;
346
+ throw new AssertionError(msg || `Expect ${format(actual)} > ${format(expected)}`);
347
+ }
348
+
349
+ export function assertGreaterOrEqual<T>(actual: T, expected: T, msg?: string): void {
350
+ if (actual >= expected) return;
351
+ throw new AssertionError(msg || `Expect ${format(actual)} >= ${format(expected)}`);
352
+ }
353
+
354
+ export function assertLess<T>(actual: T, expected: T, msg?: string): void {
355
+ if (actual < expected) return;
356
+ throw new AssertionError(msg || `Expect ${format(actual)} < ${format(expected)}`);
357
+ }
358
+
359
+ export function assertLessOrEqual<T>(actual: T, expected: T, msg?: string): void {
360
+ if (actual <= expected) return;
361
+ throw new AssertionError(msg || `Expect ${format(actual)} <= ${format(expected)}`);
362
+ }
363
+
364
+ function checkError(
365
+ err: unknown,
366
+ ErrorClass: ErrorClass | undefined,
367
+ msgIncludes: string | undefined,
368
+ msg: string | undefined,
369
+ ): void {
370
+ if (!ErrorClass) return;
371
+ if (!(err instanceof ErrorClass)) {
372
+ throw new AssertionError(
373
+ `Expected error to be instance of "${ErrorClass.name}", but was "${
374
+ err instanceof Error ? err.constructor.name : typeof err
375
+ }"${suffix(msg)}`,
376
+ );
377
+ }
378
+ if (msgIncludes !== undefined && !String((err as Error).message).includes(msgIncludes)) {
379
+ throw new AssertionError(
380
+ `Expected error message to include ${format(msgIncludes)}, but got ${format((err as Error).message)}${suffix(msg)}`,
381
+ );
382
+ }
383
+ }
384
+
385
+ /** Assert `fn` throws; returns the thrown value. */
386
+ export function assertThrows(fn: () => unknown, msg?: string): unknown;
387
+ export function assertThrows<E extends Error = Error>(
388
+ fn: () => unknown,
389
+ ErrorClass: ErrorClass<E>,
390
+ msgIncludes?: string,
391
+ msg?: string,
392
+ ): E;
393
+ export function assertThrows(
394
+ fn: () => unknown,
395
+ classOrMsg?: ErrorClass | string,
396
+ msgIncludes?: string,
397
+ msg?: string,
398
+ ): unknown {
399
+ const ErrorClass = typeof classOrMsg === "function" ? classOrMsg : undefined;
400
+ if (typeof classOrMsg === "string") msg = classOrMsg;
401
+ let thrown = false;
402
+ let error: unknown;
403
+ try {
404
+ fn();
405
+ } catch (err) {
406
+ thrown = true;
407
+ error = err;
408
+ }
409
+ if (!thrown) throw new AssertionError(`Expected function to throw${suffix(msg)}`);
410
+ checkError(error, ErrorClass, msgIncludes, msg);
411
+ return error;
412
+ }
413
+
414
+ /** Assert the promise (or `fn()`'s promise) rejects; resolves to the reason. */
415
+ export function assertRejects(fn: (() => PromiseLike<unknown>) | PromiseLike<unknown>, msg?: string): Promise<unknown>;
416
+ export function assertRejects<E extends Error = Error>(
417
+ fn: (() => PromiseLike<unknown>) | PromiseLike<unknown>,
418
+ ErrorClass: ErrorClass<E>,
419
+ msgIncludes?: string,
420
+ msg?: string,
421
+ ): Promise<E>;
422
+ export async function assertRejects(
423
+ fn: (() => PromiseLike<unknown>) | PromiseLike<unknown>,
424
+ classOrMsg?: ErrorClass | string,
425
+ msgIncludes?: string,
426
+ msg?: string,
427
+ ): Promise<unknown> {
428
+ const ErrorClass = typeof classOrMsg === "function" ? classOrMsg : undefined;
429
+ if (typeof classOrMsg === "string") msg = classOrMsg;
430
+ let p: unknown;
431
+ try {
432
+ p = typeof fn === "function" ? fn() : fn;
433
+ } catch {
434
+ // A synchronous throw is not a rejection (an async function never does this).
435
+ throw new AssertionError(`Function throws when expected to reject${suffix(msg)}`);
436
+ }
437
+ if (!p || typeof (p as PromiseLike<unknown>).then !== "function") {
438
+ throw new AssertionError(`Function returns a non-promise when expected to reject${suffix(msg)}`);
439
+ }
440
+ let rejected = false;
441
+ let error: unknown;
442
+ try {
443
+ await p;
444
+ } catch (err) {
445
+ rejected = true;
446
+ error = err;
447
+ }
448
+ if (!rejected) throw new AssertionError(`Expected function to reject${suffix(msg)}`);
449
+ checkError(error, ErrorClass, msgIncludes, msg);
450
+ return error;
451
+ }
452
+
453
+ /** Unconditionally fail. */
454
+ export function fail(msg?: string): never {
455
+ throw new AssertionError(`Failed assertion${suffix(msg)}`);
456
+ }
457
+
458
+ export function unreachable(msg?: string): never {
459
+ throw new AssertionError(msg ?? "Unreachable");
460
+ }
461
+
462
+ export function unimplemented(msg?: string): never {
463
+ throw new AssertionError(msg ?? "Unimplemented");
464
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `1tube/test` under Bun (the `bun` export condition): drives `bun:test`.
3
+ * Run with `1tube test` so function imports resolve like on the gateway.
4
+ */
5
+
6
+ import * as bun from "bun:test";
7
+ import type { DescribeFunction, Hook, TestFn, TestFunction, TestOptions, TestRuntime } from "./api.ts";
8
+
9
+ export * from "./assert.ts";
10
+ export type * from "./api.ts";
11
+
12
+ export const runtime: TestRuntime = "bun";
13
+
14
+ export const test: TestFunction = Object.assign(
15
+ (name: string, fn: TestFn, options: TestOptions = {}) => {
16
+ if (options.skip) bun.test.skip(name, fn, options.timeout);
17
+ else if (options.only) bun.test.only(name, fn, options.timeout);
18
+ else bun.test(name, fn, options.timeout);
19
+ },
20
+ {
21
+ skip: (name: string, fn: TestFn, options: TestOptions = {}) => bun.test.skip(name, fn, options.timeout),
22
+ only: (name: string, fn: TestFn, options: TestOptions = {}) => bun.test.only(name, fn, options.timeout),
23
+ todo: (name: string) => bun.test.todo(name, () => {}),
24
+ },
25
+ );
26
+
27
+ export const describe: DescribeFunction = Object.assign(
28
+ (name: string, fn: () => void) => bun.describe(name, fn),
29
+ {
30
+ skip: (name: string, fn: () => void) => bun.describe.skip(name, fn),
31
+ only: (name: string, fn: () => void) => bun.describe.only(name, fn),
32
+ },
33
+ );
34
+
35
+ export const beforeAll: Hook = (fn) => bun.beforeAll(fn);
36
+ export const afterAll: Hook = (fn) => bun.afterAll(fn);
37
+ export const beforeEach: Hook = (fn) => bun.beforeEach(fn);
38
+ export const afterEach: Hook = (fn) => bun.afterEach(fn);
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `1tube/test` everywhere else (the default export condition): drives the
3
+ * standard `node:test` runner, which Node runs natively and other runtimes
4
+ * implement for compatibility.
5
+ */
6
+
7
+ import * as node from "node:test";
8
+ import type { DescribeFunction, Hook, TestFn, TestFunction, TestOptions, TestRuntime } from "./api.ts";
9
+
10
+ export * from "./assert.ts";
11
+ export type * from "./api.ts";
12
+
13
+ export const runtime: TestRuntime = "node";
14
+
15
+ // node:test passes a context (and a `done` callback to 2-arity functions);
16
+ // the 1tube contract has neither, so wrap to keep the arity at zero.
17
+ const register = (name: string, fn: TestFn, options: TestOptions) =>
18
+ node.test(name, { timeout: options.timeout, skip: options.skip, only: options.only }, async () => {
19
+ await fn();
20
+ });
21
+
22
+ export const test: TestFunction = Object.assign(
23
+ (name: string, fn: TestFn, options: TestOptions = {}) => void register(name, fn, options),
24
+ {
25
+ skip: (name: string, fn: TestFn, options: TestOptions = {}) => void register(name, fn, { ...options, skip: true }),
26
+ only: (name: string, fn: TestFn, options: TestOptions = {}) => void register(name, fn, { ...options, only: true }),
27
+ todo: (name: string) => void node.test(name, { todo: true }),
28
+ },
29
+ );
30
+
31
+ export const describe: DescribeFunction = Object.assign(
32
+ (name: string, fn: () => void) => void node.describe(name, fn),
33
+ {
34
+ skip: (name: string, fn: () => void) => void node.describe(name, { skip: true }, fn),
35
+ only: (name: string, fn: () => void) => void node.describe(name, { only: true }, fn),
36
+ },
37
+ );
38
+
39
+ const hook = (register: (fn: () => Promise<void>) => void): Hook => (fn) =>
40
+ register(async () => {
41
+ await fn();
42
+ });
43
+
44
+ export const beforeAll: Hook = hook((fn) => node.before(fn));
45
+ export const afterAll: Hook = hook((fn) => node.after(fn));
46
+ export const beforeEach: Hook = hook((fn) => node.beforeEach(fn));
47
+ export const afterEach: Hook = hook((fn) => node.afterEach(fn));
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `bun test --preload` hook installed by `1tube test`.
3
+ *
4
+ * Gives test files the gateway's module resolution: the project's import
5
+ * map, `npm:` / `jsr:` / `https:` specifiers, and the managed dependency
6
+ * store, applied to everything under the functions dir. Tests then import
7
+ * function code exactly as the gateway would load it.
8
+ */
9
+
10
+ import { dirname, resolve } from "node:path";
11
+ import { createCompatEnvironment } from "../compat/environment.ts";
12
+ import { configureCompat } from "../compat/plugin.ts";
13
+
14
+ const functionsDir = resolve(process.env["1TUBE_FUNCTIONS_DIR"] ?? ".");
15
+ const { deps, remote, importMap } = createCompatEnvironment({ functionsDir });
16
+
17
+ configureCompat({
18
+ deps,
19
+ remote,
20
+ importMap,
21
+ functionsDir,
22
+ functionRoots: [functionsDir, ...(importMap ? [dirname(importMap.base)] : [])],
23
+ });