@ucdjs/test-utils 1.0.1-beta.1

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
+ import { n as unsafeResponse, o as MockStoreConfig, t as configure } from "./helpers-D3XLw9D1.mjs";
2
+ import { mockStoreApi } from "./mock-store.mjs";
3
+
4
+ //#region src/async.d.ts
5
+ /**
6
+ * Collects all values from an async iterable into an array.
7
+ *
8
+ * This function consumes an async iterable and collects all emitted values into
9
+ * a regular array. If the async iterable throws an error at any point, the error
10
+ * is propagated after the iterator's `return()` method is called (if available)
11
+ * to allow for cleanup.
12
+ *
13
+ * @typeParam T - The type of values yielded by the async iterable.
14
+ * @param {AsyncIterable<T>} iterable - The async iterable to consume completely.
15
+ * @returns {Promise<T[]>} A promise that resolves to an array containing all values in emission order.
16
+ * @throws {Error} Propagates any error thrown by the async iterable.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const values = await collect(asyncFromArray([1, 2, 3]));
21
+ * console.assert(values.equals([1, 2, 3]));
22
+ * ```
23
+ *
24
+ * @example Cleanup on error
25
+ * ```ts
26
+ * async function* source() {
27
+ * try {
28
+ * yield 1;
29
+ * throw new Error('boom');
30
+ * } finally {
31
+ * console.log('Cleanup!'); // Called even though an error was thrown
32
+ * }
33
+ * }
34
+ * await collect(source()); // throws, but cleanup runs first
35
+ * ```
36
+ */
37
+ declare function collect<T>(iterable: AsyncIterable<T>): Promise<T[]>;
38
+ /**
39
+ * Wraps a synchronous iterable as an async iterable.
40
+ *
41
+ * This is useful in tests when you want to simulate an async source but only have
42
+ * synchronous data. The resulting async iterable properly implements the async
43
+ * iterator protocol, including support for cleanup via `return()`.
44
+ *
45
+ * @typeParam T - The type of elements in the iterable.
46
+ * @param {Iterable<T>} iterable - A synchronous iterable (array, Set, Map, etc.) to wrap.
47
+ * @param {object} [options] - Optional configuration.
48
+ * @param {number} [options.delay] - Number of milliseconds to wait before each value is yielded.
49
+ * Useful for simulating network latency or slow producers in tests.
50
+ * Must be a non-negative number.
51
+ * @returns {AsyncIterable<T>} An async iterable that yields each element from the source iterable.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * // Simple case: wrap an array
56
+ * for await (const value of asyncFromArray([1, 2, 3])) {
57
+ * console.log(value);
58
+ * }
59
+ * ```
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * // Simulate network latency (50ms between values)
64
+ * for await (const value of asyncFromArray(['a', 'b', 'c'], { delay: 50 })) {
65
+ * console.log(value);
66
+ * }
67
+ * ```
68
+ */
69
+ declare function asyncFromArray<T>(iterable: Iterable<T>, options?: {
70
+ readonly delay?: number;
71
+ }): AsyncIterable<T>;
72
+ //#endregion
73
+ //#region src/index.d.ts
74
+ declare function encodeBase64(content: string): string;
75
+ //#endregion
76
+ export { type MockStoreConfig, asyncFromArray, collect, configure, encodeBase64, mockStoreApi, unsafeResponse };
package/dist/index.mjs ADDED
@@ -0,0 +1,89 @@
1
+ import { c as unsafeResponse, s as configure } from "./file-tree-CColHWG6.mjs";
2
+ import { mockStoreApi } from "./mock-store.mjs";
3
+
4
+ //#region src/async.ts
5
+ /**
6
+ * Collects all values from an async iterable into an array.
7
+ *
8
+ * This function consumes an async iterable and collects all emitted values into
9
+ * a regular array. If the async iterable throws an error at any point, the error
10
+ * is propagated after the iterator's `return()` method is called (if available)
11
+ * to allow for cleanup.
12
+ *
13
+ * @typeParam T - The type of values yielded by the async iterable.
14
+ * @param {AsyncIterable<T>} iterable - The async iterable to consume completely.
15
+ * @returns {Promise<T[]>} A promise that resolves to an array containing all values in emission order.
16
+ * @throws {Error} Propagates any error thrown by the async iterable.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const values = await collect(asyncFromArray([1, 2, 3]));
21
+ * console.assert(values.equals([1, 2, 3]));
22
+ * ```
23
+ *
24
+ * @example Cleanup on error
25
+ * ```ts
26
+ * async function* source() {
27
+ * try {
28
+ * yield 1;
29
+ * throw new Error('boom');
30
+ * } finally {
31
+ * console.log('Cleanup!'); // Called even though an error was thrown
32
+ * }
33
+ * }
34
+ * await collect(source()); // throws, but cleanup runs first
35
+ * ```
36
+ */
37
+ async function collect(iterable) {
38
+ const result = [];
39
+ for await (const value of iterable) result.push(value);
40
+ return result;
41
+ }
42
+ /**
43
+ * Wraps a synchronous iterable as an async iterable.
44
+ *
45
+ * This is useful in tests when you want to simulate an async source but only have
46
+ * synchronous data. The resulting async iterable properly implements the async
47
+ * iterator protocol, including support for cleanup via `return()`.
48
+ *
49
+ * @typeParam T - The type of elements in the iterable.
50
+ * @param {Iterable<T>} iterable - A synchronous iterable (array, Set, Map, etc.) to wrap.
51
+ * @param {object} [options] - Optional configuration.
52
+ * @param {number} [options.delay] - Number of milliseconds to wait before each value is yielded.
53
+ * Useful for simulating network latency or slow producers in tests.
54
+ * Must be a non-negative number.
55
+ * @returns {AsyncIterable<T>} An async iterable that yields each element from the source iterable.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * // Simple case: wrap an array
60
+ * for await (const value of asyncFromArray([1, 2, 3])) {
61
+ * console.log(value);
62
+ * }
63
+ * ```
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * // Simulate network latency (50ms between values)
68
+ * for await (const value of asyncFromArray(['a', 'b', 'c'], { delay: 50 })) {
69
+ * console.log(value);
70
+ * }
71
+ * ```
72
+ */
73
+ function asyncFromArray(iterable, options) {
74
+ return (async function* () {
75
+ for (const value of iterable) {
76
+ if (options?.delay) await new Promise((resolve) => setTimeout(resolve, options.delay));
77
+ yield value;
78
+ }
79
+ })();
80
+ }
81
+
82
+ //#endregion
83
+ //#region src/index.ts
84
+ function encodeBase64(content) {
85
+ return Buffer.from(content, "utf-8").toString("base64");
86
+ }
87
+
88
+ //#endregion
89
+ export { asyncFromArray, collect, configure, encodeBase64, mockStoreApi, unsafeResponse };
@@ -0,0 +1,149 @@
1
+ import "vitest";
2
+ import z$2 from "zod";
3
+ import "@vitest/expect";
4
+
5
+ //#region src/matchers/error-matchers.d.ts
6
+ var ErrorMatcherOptions = [
7
+ 30,
8
+ () => [
9
+ Error,
10
+ RegExp,
11
+ Error,
12
+ Record
13
+ ],
14
+ [
15
+ "",
16
+ "",
17
+ "",
18
+ "",
19
+ "",
20
+ "",
21
+ "",
22
+ "",
23
+ "",
24
+ ""
25
+ ]
26
+ ];
27
+
28
+ //#endregion
29
+ //#region src/matchers/response-matchers.d.ts
30
+ var ApiErrorOptions = [
31
+ 25,
32
+ () => [RegExp],
33
+ [
34
+ "",
35
+ "",
36
+ ""
37
+ ]
38
+ ];
39
+ var ResponseMatcherOptions = [
40
+ 28,
41
+ () => [
42
+ RegExp,
43
+ Record,
44
+ RegExp,
45
+ RegExp
46
+ ],
47
+ [
48
+ "",
49
+ "",
50
+ "",
51
+ "",
52
+ "",
53
+ "",
54
+ "",
55
+ "",
56
+ "",
57
+ "",
58
+ ""
59
+ ]
60
+ ];
61
+
62
+ //#endregion
63
+ //#region src/matchers/schema-matchers.d.ts
64
+ var SchemaMatcherOptions = [
65
+ 23,
66
+ (TSchema) => [
67
+ z$2.ZodType,
68
+ TSchema,
69
+ TSchema,
70
+ z$2.infer,
71
+ Partial
72
+ ],
73
+ [
74
+ "",
75
+ "",
76
+ "",
77
+ "",
78
+ "",
79
+ "",
80
+ "",
81
+ "",
82
+ "",
83
+ "",
84
+ ""
85
+ ]
86
+ ];
87
+
88
+ //#endregion
89
+ //#region src/matchers/types.d.ts
90
+ var CustomMatchers = [
91
+ 0,
92
+ (TSchema, R) => [
93
+ ErrorMatcherOptions,
94
+ R,
95
+ z.ZodType,
96
+ TSchema,
97
+ SchemaMatcherOptions,
98
+ R,
99
+ ApiErrorOptions,
100
+ R,
101
+ Promise,
102
+ R,
103
+ ResponseMatcherOptions,
104
+ R,
105
+ Promise
106
+ ],
107
+ [
108
+ "",
109
+ "",
110
+ "",
111
+ "",
112
+ "",
113
+ "",
114
+ "",
115
+ "",
116
+ "",
117
+ "",
118
+ "",
119
+ "",
120
+ "",
121
+ "",
122
+ "",
123
+ "",
124
+ "",
125
+ "",
126
+ "",
127
+ "",
128
+ "",
129
+ "",
130
+ "",
131
+ "",
132
+ "",
133
+ ""
134
+ ]
135
+ ];
136
+ var _0 = [
137
+ 1,
138
+ (T) => [T, CustomMatchers],
139
+ [
140
+ "",
141
+ "",
142
+ "",
143
+ ""
144
+ ],
145
+ sideEffect(_0)
146
+ ];
147
+
148
+ //#endregion
149
+ export { };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,239 @@
1
+ import { tryOr } from "@ucdjs-internal/shared";
2
+ import { expect } from "vitest";
3
+
4
+ //#region src/matchers/error-matchers.ts
5
+ const toMatchError = function(received, options) {
6
+ let error = null;
7
+ if (typeof received === "function") try {
8
+ received();
9
+ return {
10
+ pass: false,
11
+ message: () => "Expected function to throw an error, but it did not"
12
+ };
13
+ } catch (e) {
14
+ if (!(e instanceof Error)) return {
15
+ pass: false,
16
+ message: () => `Expected function to throw an Error, but it threw ${typeof e}`
17
+ };
18
+ error = e;
19
+ }
20
+ else if (received instanceof Error) error = received;
21
+ else return {
22
+ pass: false,
23
+ message: () => `Expected an Error instance or a function that throws, but received ${typeof received}`
24
+ };
25
+ const errorName = error.constructor.name;
26
+ if (options.type && !(error instanceof options.type)) return {
27
+ pass: false,
28
+ message: () => `Expected error to be instance of ${options.type.name}, but got ${errorName}`
29
+ };
30
+ if (options.message) {
31
+ if (!(typeof options.message === "string" ? error.message === options.message : options.message.test(error.message))) return {
32
+ pass: false,
33
+ message: () => `Expected error message to match ${options.message}, but got "${error.message}"`
34
+ };
35
+ }
36
+ if (options.cause) {
37
+ const causeError = error.cause ?? error.originalError;
38
+ if (!causeError) return {
39
+ pass: false,
40
+ message: () => `Expected error to have a cause, but none was found`
41
+ };
42
+ if (!(causeError instanceof options.cause)) {
43
+ const causeName = causeError instanceof Error ? causeError.constructor.name : typeof causeError;
44
+ return {
45
+ pass: false,
46
+ message: () => `Expected cause to be instance of ${options.cause.name}, but got ${causeName}`
47
+ };
48
+ }
49
+ }
50
+ if (options.fields) {
51
+ const errorRecord = error;
52
+ for (const [key, expectedValue] of Object.entries(options.fields)) {
53
+ const actualValue = errorRecord[key];
54
+ if (!(typeof expectedValue === "object" && expectedValue !== null ? JSON.stringify(actualValue) === JSON.stringify(expectedValue) : actualValue === expectedValue)) return {
55
+ pass: false,
56
+ message: () => `Expected error.${key} to be ${JSON.stringify(expectedValue)}, but got ${JSON.stringify(actualValue)}`
57
+ };
58
+ }
59
+ }
60
+ return {
61
+ pass: true,
62
+ message: () => `Expected error not to match the given criteria`
63
+ };
64
+ };
65
+
66
+ //#endregion
67
+ //#region src/matchers/response-matchers.ts
68
+ const toBeApiError = async function(received, options) {
69
+ const { isNot, equals } = this;
70
+ if (!equals(received.status, options.status)) return {
71
+ pass: false,
72
+ message: () => `Expected response to${isNot ? " not" : ""} be an API error with status ${options.status}, but got ${received.status}`
73
+ };
74
+ if (!received.headers.get("content-type")?.includes("application/json")) return {
75
+ pass: false,
76
+ message: () => `Expected response to${isNot ? " not" : ""} have application/json content-type`
77
+ };
78
+ const error = await received.json();
79
+ if (!error.status || !error.message || !error.timestamp) return {
80
+ pass: false,
81
+ message: () => `Expected response to${isNot ? " not" : ""} have status, message, and timestamp properties`
82
+ };
83
+ if (options.message) {
84
+ if (!(typeof options.message === "string" ? error.message === options.message : options.message.test(error.message))) {
85
+ const expectedMsg = typeof options.message === "string" ? options.message : options.message.source;
86
+ return {
87
+ pass: false,
88
+ message: () => `Expected error message to${isNot ? " not" : ""} match ${expectedMsg}, but got "${error.message}"`
89
+ };
90
+ }
91
+ }
92
+ return {
93
+ pass: true,
94
+ message: () => `Expected response to${isNot ? " not" : ""} be an API error`
95
+ };
96
+ };
97
+ const toBeHeadError = function(received, expectedStatus) {
98
+ const { isNot, equals } = this;
99
+ if (!equals(received.status, expectedStatus)) return {
100
+ pass: false,
101
+ message: () => `Expected HEAD response status to${isNot ? " not" : ""} be ${expectedStatus}, but got ${received.status}`
102
+ };
103
+ const contentLength = received.headers.get("content-length");
104
+ if (contentLength !== null && Number.parseInt(contentLength, 10) !== 0) return {
105
+ pass: false,
106
+ message: () => `Expected HEAD response to${isNot ? " not" : ""} have content-length of 0`
107
+ };
108
+ return {
109
+ pass: true,
110
+ message: () => `Expected HEAD response to${isNot ? " not" : ""} have status ${expectedStatus}`
111
+ };
112
+ };
113
+ const toMatchResponse = async function(received, options) {
114
+ const { isNot, equals } = this;
115
+ if (options.status !== void 0 && !equals(received.status, options.status)) return {
116
+ pass: false,
117
+ message: () => `Expected status to${isNot ? " not" : ""} be ${options.status}, but got ${received.status}`
118
+ };
119
+ const isJson = received.headers.get("content-type")?.includes("application/json");
120
+ if (options.json && !isJson) return {
121
+ pass: false,
122
+ message: () => `Expected response to${isNot ? " not" : ""} have application/json content-type`
123
+ };
124
+ if (options.cache) {
125
+ const cacheControl = received.headers.get("cache-control");
126
+ if (!cacheControl) return {
127
+ pass: false,
128
+ message: () => `Expected response to${isNot ? " not" : ""} have cache-control header`
129
+ };
130
+ if (options.cacheMaxAgePattern && !options.cacheMaxAgePattern.test(cacheControl)) return {
131
+ pass: false,
132
+ message: () => `Expected cache-control to${isNot ? " not" : ""} match ${options.cacheMaxAgePattern.source}`
133
+ };
134
+ if (!options.cacheMaxAgePattern && !/max-age=\d+/.test(cacheControl)) return {
135
+ pass: false,
136
+ message: () => `Expected cache-control to${isNot ? " not" : ""} have max-age`
137
+ };
138
+ }
139
+ if (options.headers) for (const [key, value] of Object.entries(options.headers)) {
140
+ const headerValue = received.headers.get(key);
141
+ if (!headerValue) return {
142
+ pass: false,
143
+ message: () => `Expected response to${isNot ? " not" : ""} have ${key} header`
144
+ };
145
+ if (!(typeof value === "string" ? equals(headerValue, value) : value.test(headerValue))) {
146
+ const expected = typeof value === "string" ? value : value.source;
147
+ return {
148
+ pass: false,
149
+ message: () => `Expected ${key} header to${isNot ? " not" : ""} match ${expected}, but got "${headerValue}"`
150
+ };
151
+ }
152
+ }
153
+ if (options.error) {
154
+ if (!isJson) return {
155
+ pass: false,
156
+ message: () => `Expected error response to${isNot ? " not" : ""} have application/json content-type`
157
+ };
158
+ const error = await tryOr({
159
+ try: async () => received.json(),
160
+ err(err) {
161
+ console.error("Failed to parse response JSON:", err);
162
+ return null;
163
+ }
164
+ });
165
+ if (error == null) return {
166
+ pass: false,
167
+ message: () => `Expected response body to${isNot ? " not" : ""} be valid JSON`
168
+ };
169
+ if (!error.status) return {
170
+ pass: false,
171
+ message: () => `Expected error to${isNot ? " not" : ""} have "status" property`
172
+ };
173
+ if (!error.message) return {
174
+ pass: false,
175
+ message: () => `Expected error to${isNot ? " not" : ""} have "message" property`
176
+ };
177
+ if (!error.timestamp) return {
178
+ pass: false,
179
+ message: () => `Expected error to${isNot ? " not" : ""} have "timestamp" property`
180
+ };
181
+ if (options.status !== void 0 && !equals(error.status, options.status)) return {
182
+ pass: false,
183
+ message: () => `Expected error.status to${isNot ? " not" : ""} be ${options.status}, but got ${error.status}`
184
+ };
185
+ if (options.error.message) {
186
+ if (!(options.error.message instanceof RegExp ? options.error.message.test(error.message) : equals(error.message, options.error.message))) {
187
+ const expectedMsg = typeof options.error.message === "string" ? options.error.message : options.error.message.source;
188
+ return {
189
+ pass: false,
190
+ message: () => `Expected error.message to${isNot ? " not" : ""} match "${expectedMsg}", but got "${error.message}"`
191
+ };
192
+ }
193
+ }
194
+ }
195
+ return {
196
+ pass: true,
197
+ message: () => `Expected response to${isNot ? " not" : ""} match the given criteria`
198
+ };
199
+ };
200
+
201
+ //#endregion
202
+ //#region src/matchers/schema-matchers.ts
203
+ const toMatchSchema = function(received, options) {
204
+ const result = options.schema.safeParse(received);
205
+ if (!(result.success === options.success)) {
206
+ const expectedStatus = options.success ? "succeed" : "fail";
207
+ const actualStatus = result.success ? "succeeded" : "failed";
208
+ const issues = result.error?.issues ? `\n${this.utils.printExpected(result.error.issues)}` : "";
209
+ return {
210
+ pass: false,
211
+ message: () => `Expected schema validation to ${expectedStatus}, but it ${actualStatus}${issues}`
212
+ };
213
+ }
214
+ if (options.data && result.success) for (const key of Object.keys(options.data)) {
215
+ const expected = options.data[key];
216
+ const received = result.data[key];
217
+ if (!this.equals(received, expected)) return {
218
+ pass: false,
219
+ message: () => `Expected property "${key}" to equal ${this.utils.printExpected(expected)}, but received ${this.utils.printReceived(received)}`
220
+ };
221
+ }
222
+ return {
223
+ pass: true,
224
+ message: () => `Expected schema validation to not match`
225
+ };
226
+ };
227
+
228
+ //#endregion
229
+ //#region src/matchers/vitest-setup.ts
230
+ expect.extend({
231
+ toMatchError,
232
+ toMatchSchema,
233
+ toBeApiError,
234
+ toBeHeadError,
235
+ toMatchResponse
236
+ });
237
+
238
+ //#endregion
239
+ export { };
@@ -0,0 +1,21 @@
1
+ import { a as createFileTree, i as FileTreeNodeWithContent, n as unsafeResponse, o as MockStoreConfig, r as FileTreeInput, s as MockStoreFiles, t as configure } from "./helpers-D3XLw9D1.mjs";
2
+
3
+ //#region src/mock-store/index.d.ts
4
+ declare function mockStoreApi(config?: MockStoreConfig): void;
5
+ /**
6
+ * Sets up mock handlers for the store subdomain (ucd-store.ucdjs.dev).
7
+ *
8
+ * This is used for the HTTP fs-bridge which directly accesses files via the store subdomain
9
+ * rather than through the API. The store subdomain handles paths like /:version/:filepath
10
+ * without the /ucd/ prefix (it's handled internally by the subdomain).
11
+ *
12
+ * @param {object} config - Configuration for the store subdomain mock
13
+ * @param {string} [config.storeBaseUrl] - Base URL for the store subdomain (defaults to https://ucd-store.ucdjs.dev)
14
+ * @param {MockStoreFiles} config.files - The files to mock
15
+ */
16
+ declare function mockStoreSubdomain(config: {
17
+ storeBaseUrl?: string;
18
+ files: MockStoreFiles;
19
+ }): void;
20
+ //#endregion
21
+ export { type FileTreeInput, type FileTreeNodeWithContent, type MockStoreConfig, configure, createFileTree, mockStoreApi, mockStoreSubdomain, unsafeResponse };