1tube 0.2.0 → 0.4.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,76 @@
1
+ /**
2
+ * The `1tube/test` contract every runner binding implements.
3
+ *
4
+ * Edge function tests import `test`, `describe`, hooks, `expect`, mocks and
5
+ * assertions from `1tube/test` instead of a specific runner, so the same
6
+ * files run under `bun test` today and under another runtime's runner
7
+ * (node:test, a workerd pool) later. Keep this surface to what every runner
8
+ * can express: no test context argument, no runner-specific options.
9
+ */
10
+ import type { Mock } from "./expect.ts";
11
+ export type TestFn = () => void | Promise<unknown>;
12
+ export type HookFn = () => void | Promise<unknown>;
13
+ export interface TestOptions {
14
+ /** Per-test timeout in milliseconds (runner default when omitted). */
15
+ timeout?: number;
16
+ /** Register but do not run. */
17
+ skip?: boolean;
18
+ /** Run only tests marked `only` (where the runner supports focusing). */
19
+ only?: boolean;
20
+ }
21
+ /** Row arguments for `.each`: array rows spread, anything else is one argument. */
22
+ export type EachArgs<Row> = Row extends readonly unknown[] ? Row : [Row];
23
+ export type RegisterTest = (name: string, fn: TestFn, options?: TestOptions) => void;
24
+ export interface TestFunction extends RegisterTest {
25
+ skip: RegisterTest;
26
+ only: RegisterTest;
27
+ /** A placeholder for a test that is not written yet. */
28
+ todo(name: string): void;
29
+ /** Register normally when `condition` is truthy, else as skipped. */
30
+ if(condition: unknown): RegisterTest;
31
+ /** Register as skipped when `condition` is truthy. */
32
+ skipIf(condition: unknown): RegisterTest;
33
+ /**
34
+ * One test per row. The name supports printf placeholders (`%s %d %i %f
35
+ * %j %o %p %#`) and `$key` for object rows.
36
+ */
37
+ each<Row>(table: readonly Row[]): (name: string, fn: (...args: EachArgs<Row>) => void | Promise<unknown>, options?: TestOptions) => void;
38
+ }
39
+ export type RegisterSuite = (name: string, fn: () => void) => void;
40
+ export interface DescribeFunction extends RegisterSuite {
41
+ skip: RegisterSuite;
42
+ only: RegisterSuite;
43
+ if(condition: unknown): RegisterSuite;
44
+ skipIf(condition: unknown): RegisterSuite;
45
+ each<Row>(table: readonly Row[]): (name: string, fn: (...args: EachArgs<Row>) => void) => void;
46
+ }
47
+ export type Hook = (fn: HookFn) => void;
48
+ type AnyFn = (...args: any[]) => any;
49
+ export interface MockFunction {
50
+ /** A mock function (records calls; optional implementation). */
51
+ <T extends AnyFn = AnyFn>(impl?: T): Mock<T>;
52
+ /**
53
+ * Replace a module for subsequently loaded importers. The specifier must
54
+ * be absolute — a shared helper can't know which file called it — so
55
+ * pass `import.meta.resolve("./dep.ts")` or a package name. The factory
56
+ * returns the module's exports (`default` for the default export).
57
+ */
58
+ module(specifier: string | URL, factory: () => Record<string, unknown>): void;
59
+ /** Restore every `spyOn` replacement. */
60
+ restore(): void;
61
+ }
62
+ /** Which runner the active binding drives. */
63
+ export type TestRuntime = "bun" | "node";
64
+ /** What a runner binding provides; `createTestApi` builds the rest. */
65
+ export interface RunnerBase {
66
+ test(name: string, fn: TestFn, options: TestOptions): void;
67
+ todo(name: string): void;
68
+ describe(name: string, fn: () => void, mode: "run" | "skip" | "only"): void;
69
+ beforeAll: Hook;
70
+ afterAll: Hook;
71
+ beforeEach: Hook;
72
+ afterEach: Hook;
73
+ /** `specifier` is already checked to be absolute (path, file: URL or bare). */
74
+ mockModule(specifier: string, factory: () => Record<string, unknown>): void;
75
+ }
76
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The `1tube/test` contract every runner binding implements.
3
+ *
4
+ * Edge function tests import `test`, `describe`, hooks, `expect`, mocks and
5
+ * assertions from `1tube/test` instead of a specific runner, so the same
6
+ * files run under `bun test` today and under another runtime's runner
7
+ * (node:test, a workerd pool) later. Keep this surface to what every runner
8
+ * can express: no test context argument, no runner-specific options.
9
+ */
10
+ export {};
@@ -0,0 +1,63 @@
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
+ export declare class AssertionError extends Error {
11
+ name: string;
12
+ constructor(message: string);
13
+ }
14
+ type AnyConstructor = abstract new (...args: never[]) => unknown;
15
+ type ErrorClass<E extends Error = Error> = abstract new (...args: never[]) => E;
16
+ /** Readable, deterministic rendering of a value for failure messages. */
17
+ export declare function format(value: unknown): string;
18
+ /** Line-level diff of two formatted values (`-` actual, `+` expected). */
19
+ export declare function formatDiff(actual: unknown, expected: unknown): string;
20
+ /**
21
+ * Deep structural equality: same constructor, same own enumerable keys
22
+ * (strings and symbols), recursively equal values. `NaN` equals `NaN`;
23
+ * `0` and `-0` differ. Maps and Sets compare by content, order-insensitive.
24
+ */
25
+ export declare function equal(a: unknown, b: unknown): boolean;
26
+ /** Assert that `expr` is truthy. */
27
+ export declare function assert(expr: unknown, msg?: string): asserts expr;
28
+ /** Assert that `expr` is falsy. */
29
+ export declare function assertFalse(expr: unknown, msg?: string): void;
30
+ /** Deep equality (see {@link equal}). */
31
+ export declare function assertEquals<T>(actual: T, expected: T, msg?: string): void;
32
+ export declare function assertNotEquals<T>(actual: T, expected: T, msg?: string): void;
33
+ /** Identity (`Object.is`). */
34
+ export declare function assertStrictEquals<T>(actual: unknown, expected: T, msg?: string): asserts actual is T;
35
+ export declare function assertNotStrictEquals<T>(actual: T, expected: T, msg?: string): void;
36
+ /** |actual - expected| <= tolerance (default 1e-7). */
37
+ export declare function assertAlmostEquals(actual: number, expected: number, tolerance?: number, msg?: string): void;
38
+ /** Not `null` and not `undefined`. */
39
+ export declare function assertExists<T>(actual: T, msg?: string): asserts actual is NonNullable<T>;
40
+ export declare function assertInstanceOf<T extends AnyConstructor>(actual: unknown, expectedType: T, msg?: string): asserts actual is InstanceType<T>;
41
+ export declare function assertNotInstanceOf<T extends AnyConstructor>(actual: unknown, unexpectedType: T, msg?: string): void;
42
+ export declare function assertStringIncludes(actual: string, expected: string, msg?: string): void;
43
+ export declare function assertMatch(actual: string, expected: RegExp, msg?: string): void;
44
+ export declare function assertNotMatch(actual: string, expected: RegExp, msg?: string): void;
45
+ /** Every element of `expected` is deep-equal to some element of `actual`. */
46
+ export declare function assertArrayIncludes<T>(actual: ArrayLike<T>, expected: ArrayLike<T>, msg?: string): void;
47
+ /** `actual` contains (recursively) every key/value of `expected`. */
48
+ export declare function assertObjectMatch(actual: Record<PropertyKey, unknown>, expected: Record<PropertyKey, unknown>, msg?: string): void;
49
+ export declare function assertGreater<T>(actual: T, expected: T, msg?: string): void;
50
+ export declare function assertGreaterOrEqual<T>(actual: T, expected: T, msg?: string): void;
51
+ export declare function assertLess<T>(actual: T, expected: T, msg?: string): void;
52
+ export declare function assertLessOrEqual<T>(actual: T, expected: T, msg?: string): void;
53
+ /** Assert `fn` throws; returns the thrown value. */
54
+ export declare function assertThrows(fn: () => unknown, msg?: string): unknown;
55
+ export declare function assertThrows<E extends Error = Error>(fn: () => unknown, ErrorClass: ErrorClass<E>, msgIncludes?: string, msg?: string): E;
56
+ /** Assert the promise (or `fn()`'s promise) rejects; resolves to the reason. */
57
+ export declare function assertRejects(fn: (() => PromiseLike<unknown>) | PromiseLike<unknown>, msg?: string): Promise<unknown>;
58
+ export declare function assertRejects<E extends Error = Error>(fn: (() => PromiseLike<unknown>) | PromiseLike<unknown>, ErrorClass: ErrorClass<E>, msgIncludes?: string, msg?: string): Promise<E>;
59
+ /** Unconditionally fail. */
60
+ export declare function fail(msg?: string): never;
61
+ export declare function unreachable(msg?: string): never;
62
+ export declare function unimplemented(msg?: string): never;
63
+ export {};
@@ -0,0 +1,430 @@
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
+ export class AssertionError extends Error {
11
+ name = "AssertionError";
12
+ constructor(message) {
13
+ super(message);
14
+ }
15
+ }
16
+ // ---------------------------------------------------------------------------
17
+ // Formatting
18
+ // ---------------------------------------------------------------------------
19
+ /** Readable, deterministic rendering of a value for failure messages. */
20
+ export function format(value) {
21
+ const seen = new WeakSet();
22
+ const indent = (s) => s.replace(/\n/g, "\n ");
23
+ const render = (v) => {
24
+ switch (typeof v) {
25
+ case "string":
26
+ return JSON.stringify(v);
27
+ case "bigint":
28
+ return `${v}n`;
29
+ case "symbol":
30
+ case "undefined":
31
+ case "boolean":
32
+ return String(v);
33
+ case "number":
34
+ return Object.is(v, -0) ? "-0" : String(v);
35
+ case "function":
36
+ return `[Function: ${v.name || "anonymous"}]`;
37
+ }
38
+ if (v === null)
39
+ return "null";
40
+ const obj = v;
41
+ if (seen.has(obj))
42
+ return "[Circular]";
43
+ if (obj instanceof Date)
44
+ return `Date(${Number.isNaN(obj.getTime()) ? "Invalid" : obj.toISOString()})`;
45
+ if (obj instanceof RegExp)
46
+ return String(obj);
47
+ if (obj instanceof Error)
48
+ return `${obj.name}: ${obj.message}`;
49
+ if (typeof URL !== "undefined" && obj instanceof URL)
50
+ return `URL(${obj.href})`;
51
+ seen.add(obj);
52
+ try {
53
+ if (Array.isArray(obj)) {
54
+ if (obj.length === 0)
55
+ return "[]";
56
+ return `[\n ${obj.map((x) => indent(render(x))).join(",\n ")},\n]`;
57
+ }
58
+ if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
59
+ const items = Array.from(obj, (x) => render(x));
60
+ return `${obj.constructor.name}(${items.length}) [${items.join(", ")}]`;
61
+ }
62
+ if (obj instanceof Map) {
63
+ if (obj.size === 0)
64
+ return "Map(0) {}";
65
+ const rows = [...obj].map(([k, x]) => `${indent(render(k))} => ${indent(render(x))}`);
66
+ return `Map(${obj.size}) {\n ${rows.join(",\n ")},\n}`;
67
+ }
68
+ if (obj instanceof Set) {
69
+ if (obj.size === 0)
70
+ return "Set(0) {}";
71
+ return `Set(${obj.size}) {\n ${[...obj].map((x) => indent(render(x))).join(",\n ")},\n}`;
72
+ }
73
+ const keys = [
74
+ ...Object.keys(obj),
75
+ ...Object.getOwnPropertySymbols(obj).filter((s) => Object.prototype.propertyIsEnumerable.call(obj, s)),
76
+ ];
77
+ const proto = Object.getPrototypeOf(obj);
78
+ const tag = proto === null
79
+ ? "[Object: null prototype] "
80
+ : proto !== Object.prototype && proto?.constructor?.name
81
+ ? `${proto.constructor.name} `
82
+ : "";
83
+ if (keys.length === 0)
84
+ return `${tag}{}`;
85
+ const rows = keys.map((k) => {
86
+ const name = typeof k === "symbol"
87
+ ? `[${String(k)}]`
88
+ : /^[A-Za-z_$][\w$]*$/.test(k)
89
+ ? k
90
+ : JSON.stringify(k);
91
+ return `${name}: ${indent(render(obj[k]))}`;
92
+ });
93
+ return `${tag}{\n ${rows.join(",\n ")},\n}`;
94
+ }
95
+ finally {
96
+ seen.delete(obj);
97
+ }
98
+ };
99
+ return render(value);
100
+ }
101
+ /** Line-level diff of two formatted values (`-` actual, `+` expected). */
102
+ export function formatDiff(actual, expected) {
103
+ const a = format(actual).split("\n");
104
+ const b = format(expected).split("\n");
105
+ // LCS over lines: values printed for assertion failures are small.
106
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
107
+ for (let i = a.length - 1; i >= 0; i--) {
108
+ for (let j = b.length - 1; j >= 0; j--) {
109
+ lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
110
+ }
111
+ }
112
+ const out = [];
113
+ let i = 0;
114
+ let j = 0;
115
+ while (i < a.length || j < b.length) {
116
+ if (i < a.length && j < b.length && a[i] === b[j]) {
117
+ out.push(` ${a[i]}`);
118
+ i++;
119
+ j++;
120
+ }
121
+ else if (j < b.length && (i >= a.length || lcs[i][j + 1] >= lcs[i + 1][j])) {
122
+ out.push(`+ ${b[j++]}`);
123
+ }
124
+ else {
125
+ out.push(`- ${a[i++]}`);
126
+ }
127
+ }
128
+ return out.join("\n");
129
+ }
130
+ const suffix = (msg) => (msg ? `: ${msg}` : "");
131
+ // ---------------------------------------------------------------------------
132
+ // Equality
133
+ // ---------------------------------------------------------------------------
134
+ /**
135
+ * Deep structural equality: same constructor, same own enumerable keys
136
+ * (strings and symbols), recursively equal values. `NaN` equals `NaN`;
137
+ * `0` and `-0` differ. Maps and Sets compare by content, order-insensitive.
138
+ */
139
+ export function equal(a, b) {
140
+ const seen = new Map();
141
+ const eq = (x, y) => {
142
+ if (Object.is(x, y))
143
+ return true;
144
+ if (typeof x !== "object" || typeof y !== "object" || x === null || y === null) {
145
+ return false;
146
+ }
147
+ if (seen.get(x) === y)
148
+ return true;
149
+ if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y))
150
+ return false;
151
+ seen.set(x, y);
152
+ if (x instanceof Date)
153
+ return Object.is(x.getTime(), y.getTime());
154
+ if (x instanceof RegExp)
155
+ return String(x) === String(y);
156
+ if (typeof URL !== "undefined" && x instanceof URL)
157
+ return x.href === y.href;
158
+ if (x instanceof WeakMap || x instanceof WeakSet || x instanceof WeakRef) {
159
+ throw new TypeError("cannot compare WeakMap, WeakSet or WeakRef instances");
160
+ }
161
+ if (x instanceof Number || x instanceof String || x instanceof Boolean) {
162
+ if (!Object.is(x.valueOf(), y.valueOf()))
163
+ return false;
164
+ }
165
+ if (x instanceof ArrayBuffer) {
166
+ return eq(new Uint8Array(x), new Uint8Array(y));
167
+ }
168
+ if (x instanceof DataView) {
169
+ return eq(new Uint8Array(x.buffer, x.byteOffset, x.byteLength), new Uint8Array(y.buffer, y.byteOffset, y.byteLength));
170
+ }
171
+ if (ArrayBuffer.isView(x)) {
172
+ const xa = x;
173
+ const ya = y;
174
+ if (xa.length !== ya.length)
175
+ return false;
176
+ for (let i = 0; i < xa.length; i++)
177
+ if (!Object.is(xa[i], ya[i]))
178
+ return false;
179
+ return true;
180
+ }
181
+ if (x instanceof Map) {
182
+ const ym = y;
183
+ if (x.size !== ym.size)
184
+ return false;
185
+ const unmatched = [...ym];
186
+ for (const [k, v] of x) {
187
+ const idx = unmatched.findIndex(([k2, v2]) => eq(k, k2) && eq(v, v2));
188
+ if (idx === -1)
189
+ return false;
190
+ unmatched.splice(idx, 1);
191
+ }
192
+ return true;
193
+ }
194
+ if (x instanceof Set) {
195
+ const ys = y;
196
+ if (x.size !== ys.size)
197
+ return false;
198
+ const unmatched = [...ys];
199
+ for (const v of x) {
200
+ const idx = unmatched.findIndex((v2) => eq(v, v2));
201
+ if (idx === -1)
202
+ return false;
203
+ unmatched.splice(idx, 1);
204
+ }
205
+ return true;
206
+ }
207
+ if (x instanceof Error) {
208
+ const ye = y;
209
+ if (x.name !== ye.name || x.message !== ye.message)
210
+ return false;
211
+ }
212
+ const keysOf = (o) => [
213
+ ...Object.keys(o),
214
+ ...Object.getOwnPropertySymbols(o).filter((s) => Object.prototype.propertyIsEnumerable.call(o, s)),
215
+ ];
216
+ const xk = keysOf(x);
217
+ const yk = keysOf(y);
218
+ if (xk.length !== yk.length)
219
+ return false;
220
+ const rec = (o, k) => o[k];
221
+ for (const k of xk) {
222
+ if (!Object.prototype.hasOwnProperty.call(y, k))
223
+ return false;
224
+ if (!eq(rec(x, k), rec(y, k)))
225
+ return false;
226
+ }
227
+ return true;
228
+ };
229
+ return eq(a, b);
230
+ }
231
+ // ---------------------------------------------------------------------------
232
+ // Assertions
233
+ // ---------------------------------------------------------------------------
234
+ /** Assert that `expr` is truthy. */
235
+ export function assert(expr, msg = "") {
236
+ if (!expr)
237
+ throw new AssertionError(msg || "Expected value to be truthy");
238
+ }
239
+ /** Assert that `expr` is falsy. */
240
+ export function assertFalse(expr, msg = "") {
241
+ if (expr)
242
+ throw new AssertionError(msg || `Expected value to be falsy, got ${format(expr)}`);
243
+ }
244
+ /** Deep equality (see {@link equal}). */
245
+ export function assertEquals(actual, expected, msg) {
246
+ if (equal(actual, expected))
247
+ return;
248
+ throw new AssertionError(`Values are not equal${suffix(msg)}\n\n [Diff] Actual / Expected\n\n${formatDiff(actual, expected)}\n`);
249
+ }
250
+ export function assertNotEquals(actual, expected, msg) {
251
+ if (!equal(actual, expected))
252
+ return;
253
+ throw new AssertionError(`Expected actual: ${format(actual)} not to be: ${format(expected)}${suffix(msg)}`);
254
+ }
255
+ /** Identity (`Object.is`). */
256
+ export function assertStrictEquals(actual, expected, msg) {
257
+ if (Object.is(actual, expected))
258
+ return;
259
+ const sameRendering = format(actual) === format(expected);
260
+ throw new AssertionError(sameRendering
261
+ ? `Values have the same structure but are not reference-equal${suffix(msg)}\n\n ${format(actual).replace(/\n/g, "\n ")}\n`
262
+ : `Values are not strictly equal${suffix(msg)}\n\n [Diff] Actual / Expected\n\n${formatDiff(actual, expected)}\n`);
263
+ }
264
+ export function assertNotStrictEquals(actual, expected, msg) {
265
+ if (!Object.is(actual, expected))
266
+ return;
267
+ throw new AssertionError(`Expected "actual" to not be strictly equal to: ${format(expected)}${suffix(msg)}`);
268
+ }
269
+ /** |actual - expected| <= tolerance (default 1e-7). */
270
+ export function assertAlmostEquals(actual, expected, tolerance = 1e-7, msg) {
271
+ if (Object.is(actual, expected))
272
+ return;
273
+ const delta = Math.abs(expected - actual);
274
+ if (delta <= tolerance)
275
+ return;
276
+ throw new AssertionError(`Expected actual: "${actual}" to be close to "${expected}": delta "${delta}" is greater than "${tolerance}"${suffix(msg)}`);
277
+ }
278
+ /** Not `null` and not `undefined`. */
279
+ export function assertExists(actual, msg) {
280
+ if (actual === undefined || actual === null) {
281
+ throw new AssertionError(msg || `Expected actual: "${actual}" to not be null or undefined`);
282
+ }
283
+ }
284
+ export function assertInstanceOf(actual, expectedType, msg) {
285
+ if (actual instanceof expectedType)
286
+ return;
287
+ const got = actual === null
288
+ ? "null"
289
+ : typeof actual === "object"
290
+ ? (Object.getPrototypeOf(actual)?.constructor?.name ?? "Object")
291
+ : typeof actual;
292
+ throw new AssertionError(`Expected object to be an instance of "${expectedType.name}" but was "${got}"${suffix(msg)}`);
293
+ }
294
+ export function assertNotInstanceOf(actual, unexpectedType, msg) {
295
+ if (!(actual instanceof unexpectedType))
296
+ return;
297
+ throw new AssertionError(`Expected object to not be an instance of "${unexpectedType.name}"${suffix(msg)}`);
298
+ }
299
+ export function assertStringIncludes(actual, expected, msg) {
300
+ if (typeof actual === "string" && actual.includes(expected))
301
+ return;
302
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to contain: ${format(expected)}`);
303
+ }
304
+ export function assertMatch(actual, expected, msg) {
305
+ expected.lastIndex = 0;
306
+ if (expected.test(actual))
307
+ return;
308
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to match: ${expected}`);
309
+ }
310
+ export function assertNotMatch(actual, expected, msg) {
311
+ expected.lastIndex = 0;
312
+ if (!expected.test(actual))
313
+ return;
314
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to not match: ${expected}`);
315
+ }
316
+ /** Every element of `expected` is deep-equal to some element of `actual`. */
317
+ export function assertArrayIncludes(actual, expected, msg) {
318
+ const missing = [];
319
+ for (const e of Array.from(expected)) {
320
+ if (!Array.from(actual).some((a) => equal(a, e)))
321
+ missing.push(e);
322
+ }
323
+ if (missing.length === 0)
324
+ return;
325
+ throw new AssertionError(msg || `Expected actual: ${format(actual)} to include: ${format(expected)}\nmissing: ${format(missing)}`);
326
+ }
327
+ /** `actual` contains (recursively) every key/value of `expected`. */
328
+ export function assertObjectMatch(actual, expected, msg) {
329
+ const pick = (a, e) => {
330
+ if (Array.isArray(e) && Array.isArray(a))
331
+ 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 = {};
334
+ for (const k of Reflect.ownKeys(e)) {
335
+ if (k in a)
336
+ out[k] = pick(a[k], e[k]);
337
+ }
338
+ return out;
339
+ }
340
+ return a;
341
+ };
342
+ assertEquals(pick(actual, expected), expected, msg);
343
+ }
344
+ export function assertGreater(actual, expected, msg) {
345
+ if (actual > expected)
346
+ return;
347
+ throw new AssertionError(msg || `Expect ${format(actual)} > ${format(expected)}`);
348
+ }
349
+ export function assertGreaterOrEqual(actual, expected, msg) {
350
+ if (actual >= expected)
351
+ return;
352
+ throw new AssertionError(msg || `Expect ${format(actual)} >= ${format(expected)}`);
353
+ }
354
+ export function assertLess(actual, expected, msg) {
355
+ if (actual < expected)
356
+ return;
357
+ throw new AssertionError(msg || `Expect ${format(actual)} < ${format(expected)}`);
358
+ }
359
+ export function assertLessOrEqual(actual, expected, msg) {
360
+ if (actual <= expected)
361
+ return;
362
+ throw new AssertionError(msg || `Expect ${format(actual)} <= ${format(expected)}`);
363
+ }
364
+ function checkError(err, ErrorClass, msgIncludes, msg) {
365
+ if (!ErrorClass)
366
+ return;
367
+ if (!(err instanceof ErrorClass)) {
368
+ throw new AssertionError(`Expected error to be instance of "${ErrorClass.name}", but was "${err instanceof Error ? err.constructor.name : typeof err}"${suffix(msg)}`);
369
+ }
370
+ if (msgIncludes !== undefined && !String(err.message).includes(msgIncludes)) {
371
+ throw new AssertionError(`Expected error message to include ${format(msgIncludes)}, but got ${format(err.message)}${suffix(msg)}`);
372
+ }
373
+ }
374
+ export function assertThrows(fn, classOrMsg, msgIncludes, msg) {
375
+ const ErrorClass = typeof classOrMsg === "function" ? classOrMsg : undefined;
376
+ if (typeof classOrMsg === "string")
377
+ msg = classOrMsg;
378
+ let thrown = false;
379
+ let error;
380
+ try {
381
+ fn();
382
+ }
383
+ catch (err) {
384
+ thrown = true;
385
+ error = err;
386
+ }
387
+ if (!thrown)
388
+ throw new AssertionError(`Expected function to throw${suffix(msg)}`);
389
+ checkError(error, ErrorClass, msgIncludes, msg);
390
+ return error;
391
+ }
392
+ export async function assertRejects(fn, classOrMsg, msgIncludes, msg) {
393
+ const ErrorClass = typeof classOrMsg === "function" ? classOrMsg : undefined;
394
+ if (typeof classOrMsg === "string")
395
+ msg = classOrMsg;
396
+ let p;
397
+ try {
398
+ p = typeof fn === "function" ? fn() : fn;
399
+ }
400
+ catch {
401
+ // A synchronous throw is not a rejection (an async function never does this).
402
+ throw new AssertionError(`Function throws when expected to reject${suffix(msg)}`);
403
+ }
404
+ if (!p || typeof p.then !== "function") {
405
+ throw new AssertionError(`Function returns a non-promise when expected to reject${suffix(msg)}`);
406
+ }
407
+ let rejected = false;
408
+ let error;
409
+ try {
410
+ await p;
411
+ }
412
+ catch (err) {
413
+ rejected = true;
414
+ error = err;
415
+ }
416
+ if (!rejected)
417
+ throw new AssertionError(`Expected function to reject${suffix(msg)}`);
418
+ checkError(error, ErrorClass, msgIncludes, msg);
419
+ return error;
420
+ }
421
+ /** Unconditionally fail. */
422
+ export function fail(msg) {
423
+ throw new AssertionError(`Failed assertion${suffix(msg)}`);
424
+ }
425
+ export function unreachable(msg) {
426
+ throw new AssertionError(msg ?? "Unreachable");
427
+ }
428
+ export function unimplemented(msg) {
429
+ throw new AssertionError(msg ?? "Unimplemented");
430
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Builds the full `1tube/test` surface from a runner's minimal base, so
3
+ * `.each`, `.if`/`.skipIf`, `it`, mocks and title formatting behave the
4
+ * same on every runner.
5
+ */
6
+ import type { DescribeFunction, Hook, MockFunction, RunnerBase, TestFunction } from "./api.ts";
7
+ export interface TestApi {
8
+ test: TestFunction;
9
+ it: TestFunction;
10
+ describe: DescribeFunction;
11
+ beforeAll: Hook;
12
+ afterAll: Hook;
13
+ beforeEach: Hook;
14
+ afterEach: Hook;
15
+ mock: MockFunction;
16
+ }
17
+ /** jest-style `.each` titles: printf placeholders, or `$path` for object rows. */
18
+ export declare function formatTitle(title: string, args: unknown[], index: number): string;
19
+ export declare function createTestApi(base: RunnerBase): TestApi;