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,100 @@
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 { format } from "./assert.js";
7
+ import { createMock, restoreAllMocks } from "./expect.js";
8
+ const rowArgs = (row) => (Array.isArray(row) ? row : [row]);
9
+ /** jest-style `.each` titles: printf placeholders, or `$path` for object rows. */
10
+ export function formatTitle(title, args, index) {
11
+ const [first] = args;
12
+ if (args.length === 1 && first && typeof first === "object" && !Array.isArray(first) && /\$[A-Za-z_]/.test(title)) {
13
+ return title.replace(/\$([A-Za-z_$][\w$]*(?:\.[\w$]+)*)/g, (m, path) => {
14
+ let cur = first;
15
+ for (const p of path.split(".")) {
16
+ if (cur === null || cur === undefined)
17
+ return m;
18
+ cur = cur[p];
19
+ }
20
+ return typeof cur === "string" ? cur : format(cur);
21
+ });
22
+ }
23
+ let i = 0;
24
+ return title.replace(/%[sdifjoOp#%]/g, (m) => {
25
+ if (m === "%%")
26
+ return "%";
27
+ if (m === "%#")
28
+ return String(index);
29
+ if (i >= args.length)
30
+ return m;
31
+ const v = args[i++];
32
+ switch (m) {
33
+ case "%s":
34
+ return typeof v === "string" ? v : format(v);
35
+ case "%d":
36
+ case "%i":
37
+ return String(typeof v === "bigint" ? v : Math.trunc(Number(v)));
38
+ case "%f":
39
+ return String(Number(v));
40
+ case "%j":
41
+ return JSON.stringify(v);
42
+ default:
43
+ return format(v);
44
+ }
45
+ });
46
+ }
47
+ export function createTestApi(base) {
48
+ const register = (mode) => (name, fn, options = {}) => base.test(name, fn, {
49
+ ...options,
50
+ skip: mode === "skip" || options.skip,
51
+ only: mode === "only" || options.only,
52
+ });
53
+ const test = Object.assign(register("run"), {
54
+ skip: register("skip"),
55
+ only: register("only"),
56
+ todo: (name) => base.todo(name),
57
+ if: (condition) => register(condition ? "run" : "skip"),
58
+ skipIf: (condition) => register(condition ? "skip" : "run"),
59
+ each: (table) => (name, fn, options) => {
60
+ table.forEach((row, index) => {
61
+ const args = rowArgs(row);
62
+ register("run")(formatTitle(name, args, index), (() => fn(...args)), options);
63
+ });
64
+ },
65
+ });
66
+ const suite = (mode) => (name, fn) => base.describe(name, fn, mode);
67
+ const describe = Object.assign(suite("run"), {
68
+ skip: suite("skip"),
69
+ only: suite("only"),
70
+ if: (condition) => suite(condition ? "run" : "skip"),
71
+ skipIf: (condition) => suite(condition ? "skip" : "run"),
72
+ each: (table) => (name, fn) => {
73
+ table.forEach((row, index) => {
74
+ const args = rowArgs(row);
75
+ suite("run")(formatTitle(name, args, index), () => fn(...args));
76
+ });
77
+ },
78
+ });
79
+ const mock = Object.assign((impl) => createMock(impl), {
80
+ module: (specifier, factory) => {
81
+ const s = specifier instanceof URL ? specifier.href : String(specifier);
82
+ if (s.startsWith("./") || s.startsWith("../") || s === "." || s === "..") {
83
+ throw new Error(`mock.module(${JSON.stringify(s)}): relative specifiers depend on the calling file, which a ` +
84
+ `shared helper can't see. Pass import.meta.resolve(${JSON.stringify(s)}) instead.`);
85
+ }
86
+ base.mockModule(s, factory);
87
+ },
88
+ restore: () => restoreAllMocks(),
89
+ });
90
+ return {
91
+ test,
92
+ it: test,
93
+ describe,
94
+ beforeAll: base.beforeAll,
95
+ afterAll: base.afterAll,
96
+ beforeEach: base.beforeEach,
97
+ afterEach: base.afterEach,
98
+ mock,
99
+ };
100
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Runtime-agnostic `expect`, mock functions and spies for `1tube/test`.
3
+ *
4
+ * Pure TypeScript with no runtime imports — the same semantics under every
5
+ * runner. Matchers follow the jest conventions Bun's `bun:test` also uses
6
+ * (`toEqual` ignores `undefined` properties and object types,
7
+ * `toStrictEqual` does not, `toMatchObject` matches subsets, asymmetric
8
+ * matchers like `expect.objectContaining` nest anywhere), so tests written
9
+ * against `bun:test` port by changing the import line.
10
+ */
11
+ declare const ASYMMETRIC: unique symbol;
12
+ export interface AsymmetricMatcher {
13
+ readonly [ASYMMETRIC]: true;
14
+ asymmetricMatch(other: unknown): boolean;
15
+ toString(): string;
16
+ }
17
+ /**
18
+ * jest-style deep equality. `strict` (toStrictEqual) additionally checks
19
+ * constructors, `undefined`-valued keys and array holes.
20
+ */
21
+ export declare function deepEquals(a: unknown, b: unknown, strict?: boolean): boolean;
22
+ type AnyFn = (...args: any[]) => any;
23
+ export type MockResult<T> = {
24
+ type: "return";
25
+ value: T;
26
+ } | {
27
+ type: "throw";
28
+ value: unknown;
29
+ } | {
30
+ type: "incomplete";
31
+ value: undefined;
32
+ };
33
+ export interface MockState<T extends AnyFn> {
34
+ calls: Parameters<T>[];
35
+ results: MockResult<ReturnType<T>>[];
36
+ instances: unknown[];
37
+ lastCall: Parameters<T> | undefined;
38
+ }
39
+ export type Mock<T extends AnyFn = AnyFn> = T & {
40
+ readonly mock: MockState<T>;
41
+ mockImplementation(fn: T): Mock<T>;
42
+ mockImplementationOnce(fn: T): Mock<T>;
43
+ mockReturnValue(value: ReturnType<T>): Mock<T>;
44
+ mockReturnValueOnce(value: ReturnType<T>): Mock<T>;
45
+ mockResolvedValue(value: Awaited<ReturnType<T>>): Mock<T>;
46
+ mockResolvedValueOnce(value: Awaited<ReturnType<T>>): Mock<T>;
47
+ mockRejectedValue(value: unknown): Mock<T>;
48
+ mockRejectedValueOnce(value: unknown): Mock<T>;
49
+ mockName(name: string): Mock<T>;
50
+ getMockName(): string;
51
+ /** Forget recorded calls and results. */
52
+ mockClear(): Mock<T>;
53
+ /** mockClear() plus drop implementations. */
54
+ mockReset(): Mock<T>;
55
+ /** For spies: put the original method back. */
56
+ mockRestore(): void;
57
+ };
58
+ /** Create a mock function, optionally wrapping an implementation. */
59
+ export declare function createMock<T extends AnyFn = AnyFn>(impl?: T, restore?: () => void): Mock<T>;
60
+ export declare function isMock(v: unknown): v is Mock;
61
+ /** Replace `obj[key]` with a mock that calls through to the original. */
62
+ export declare function spyOn<O extends object, K extends keyof O>(obj: O, key: K): O[K] extends AnyFn ? Mock<O[K]> : never;
63
+ /** Restore every active spy (`mock.restore()`). */
64
+ export declare function restoreAllMocks(): void;
65
+ export interface Matchers<R> {
66
+ not: Matchers<R>;
67
+ toBe(expected: unknown): R;
68
+ toEqual(expected: unknown): R;
69
+ toStrictEqual(expected: unknown): R;
70
+ toMatchObject(expected: object): R;
71
+ toBeNull(): R;
72
+ toBeUndefined(): R;
73
+ toBeDefined(): R;
74
+ toBeTruthy(): R;
75
+ toBeFalsy(): R;
76
+ toBeNaN(): R;
77
+ toBeTypeOf(type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"): R;
78
+ toBeInstanceOf(ctor: abstract new (...args: never[]) => unknown): R;
79
+ toBeGreaterThan(n: number | bigint): R;
80
+ toBeGreaterThanOrEqual(n: number | bigint): R;
81
+ toBeLessThan(n: number | bigint): R;
82
+ toBeLessThanOrEqual(n: number | bigint): R;
83
+ toBeCloseTo(n: number, numDigits?: number): R;
84
+ toBeOneOf(values: readonly unknown[]): R;
85
+ toContain(item: unknown): R;
86
+ toContainEqual(item: unknown): R;
87
+ toInclude(substring: string): R;
88
+ toStartWith(prefix: string): R;
89
+ toEndWith(suffix: string): R;
90
+ toMatch(pattern: string | RegExp): R;
91
+ toHaveLength(n: number): R;
92
+ toHaveProperty(path: string | readonly (string | number)[], value?: unknown): R;
93
+ toBeEmpty(): R;
94
+ toThrow(expected?: string | RegExp | Error | (abstract new (...args: never[]) => unknown) | AsymmetricMatcher): R;
95
+ toThrowError(expected?: string | RegExp | Error | (abstract new (...args: never[]) => unknown) | AsymmetricMatcher): R;
96
+ toHaveBeenCalled(): R;
97
+ toHaveBeenCalledTimes(n: number): R;
98
+ toHaveBeenCalledWith(...args: unknown[]): R;
99
+ toHaveBeenLastCalledWith(...args: unknown[]): R;
100
+ toHaveBeenNthCalledWith(n: number, ...args: unknown[]): R;
101
+ toHaveReturned(): R;
102
+ toHaveReturnedTimes(n: number): R;
103
+ toHaveReturnedWith(value: unknown): R;
104
+ }
105
+ export interface Assertion extends Matchers<void> {
106
+ /** Await the promise, then match its value. */
107
+ resolves: Matchers<Promise<void>>;
108
+ /** Await the promise, then match its rejection reason. */
109
+ rejects: Matchers<Promise<void>>;
110
+ }
111
+ export interface Expect {
112
+ (actual: unknown, message?: string): Assertion;
113
+ anything(): AsymmetricMatcher;
114
+ any(ctor: unknown): AsymmetricMatcher;
115
+ objectContaining(obj: object): AsymmetricMatcher;
116
+ arrayContaining(items: readonly unknown[]): AsymmetricMatcher;
117
+ stringContaining(s: string): AsymmetricMatcher;
118
+ stringMatching(p: string | RegExp): AsymmetricMatcher;
119
+ closeTo(n: number, numDigits?: number): AsymmetricMatcher;
120
+ not: {
121
+ objectContaining(obj: object): AsymmetricMatcher;
122
+ arrayContaining(items: readonly unknown[]): AsymmetricMatcher;
123
+ stringContaining(s: string): AsymmetricMatcher;
124
+ stringMatching(p: string | RegExp): AsymmetricMatcher;
125
+ };
126
+ }
127
+ export declare const expect: Expect;
128
+ export {};