bupkis 0.0.2

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,284 @@
1
+ /**
2
+ * Asynchronous assertion implementations.
3
+ *
4
+ * This module contains all built-in asynchronous assertion implementations for
5
+ * working with Promises and async operations. It provides assertions for
6
+ * Promise resolution, rejection, and async function behavior validation with
7
+ * comprehensive error handling.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ import { inspect } from 'node:util';
13
+ import { z } from 'zod/v4';
14
+
15
+ import { isA, isNonNullObject, isString } from '../../guards.js';
16
+ import {
17
+ ClassSchema,
18
+ FunctionSchema,
19
+ WrappedPromiseLikeSchema,
20
+ } from '../../schema.js';
21
+ import { valueToSchema } from '../../util.js';
22
+ import { createAsyncAssertion } from '../create.js';
23
+
24
+ const trapAsyncFnError = async (fn: () => unknown) => {
25
+ try {
26
+ await fn();
27
+ } catch (err) {
28
+ return err;
29
+ }
30
+ };
31
+
32
+ const trapPromiseError = async (promise: PromiseLike<unknown>) => {
33
+ try {
34
+ await promise;
35
+ } catch (err) {
36
+ return err;
37
+ }
38
+ };
39
+
40
+ export const AsyncAssertions = [
41
+ createAsyncAssertion(
42
+ [FunctionSchema, ['to resolve', 'to fulfill']],
43
+ async (subject) => {
44
+ try {
45
+ await subject();
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ },
51
+ ),
52
+ createAsyncAssertion(
53
+ [WrappedPromiseLikeSchema, ['to resolve', 'to fulfill']],
54
+ async (subject) => {
55
+ try {
56
+ await subject;
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ },
62
+ ),
63
+
64
+ // Non-parameterized "to reject" assertions
65
+ createAsyncAssertion([FunctionSchema, 'to reject'], async (subject) => {
66
+ let rejected = false;
67
+ try {
68
+ await subject();
69
+ } catch {
70
+ rejected = true;
71
+ }
72
+ return rejected;
73
+ }),
74
+ createAsyncAssertion(
75
+ [WrappedPromiseLikeSchema, 'to reject'],
76
+ async (subject) => {
77
+ let rejected = false;
78
+ try {
79
+ await subject;
80
+ } catch {
81
+ rejected = true;
82
+ }
83
+ return rejected;
84
+ },
85
+ ),
86
+ // Parameterized "to reject" with class constructor
87
+ createAsyncAssertion(
88
+ [FunctionSchema, ['to reject with a', 'to reject with an'], ClassSchema],
89
+ async (subject, ctor) => {
90
+ const error = await trapAsyncFnError(subject);
91
+ if (!error) {
92
+ return false;
93
+ }
94
+ return isA(error, ctor);
95
+ },
96
+ ),
97
+ createAsyncAssertion(
98
+ [
99
+ WrappedPromiseLikeSchema,
100
+ ['to reject with a', 'to reject with an'],
101
+ ClassSchema,
102
+ ],
103
+ async (subject, ctor) => {
104
+ const error = await trapPromiseError(subject);
105
+ if (!error) {
106
+ return false;
107
+ }
108
+ return isA(error, ctor);
109
+ },
110
+ ),
111
+
112
+ // Parameterized "to reject" with string, RegExp, or object patterns
113
+ createAsyncAssertion(
114
+ [
115
+ FunctionSchema,
116
+ ['to reject with'],
117
+ z.union([z.string(), z.instanceof(RegExp), z.looseObject({})]),
118
+ ],
119
+ async (subject, param) => {
120
+ const error = await trapAsyncFnError(subject);
121
+ if (!error) {
122
+ return false;
123
+ }
124
+
125
+ if (isString(param)) {
126
+ return z
127
+ .object({
128
+ message: z.coerce.string().pipe(z.literal(param)),
129
+ })
130
+ .or(z.coerce.string().pipe(z.literal(param)))
131
+ .safeParse(error).success;
132
+ } else if (isA(param, RegExp)) {
133
+ return z
134
+ .object({
135
+ message: z.coerce.string().regex(param),
136
+ })
137
+ .or(z.coerce.string().regex(param))
138
+ .safeParse(error).success;
139
+ } else if (isNonNullObject(param)) {
140
+ return valueToSchema(param, { strict: false }).safeParse(error).success;
141
+ } else {
142
+ throw new TypeError(`Invalid parameter schema: ${inspect(param)}`);
143
+ }
144
+ },
145
+ ),
146
+ createAsyncAssertion(
147
+ [
148
+ WrappedPromiseLikeSchema,
149
+ ['to reject with'],
150
+ z.union([z.string(), z.instanceof(RegExp), z.looseObject({})]),
151
+ ],
152
+ async (subject, param) => {
153
+ const error = await trapPromiseError(subject);
154
+ if (!error) {
155
+ return false;
156
+ }
157
+
158
+ if (isString(param)) {
159
+ return z
160
+ .object({
161
+ message: z.coerce.string().pipe(z.literal(param)),
162
+ })
163
+ .or(z.coerce.string().pipe(z.literal(param)))
164
+ .safeParse(error).success;
165
+ } else if (isA(param, RegExp)) {
166
+ return z
167
+ .object({
168
+ message: z.coerce.string().regex(param),
169
+ })
170
+ .or(z.coerce.string().regex(param))
171
+ .safeParse(error).success;
172
+ } else if (isNonNullObject(param)) {
173
+ return valueToSchema(param, { strict: false }).safeParse(error).success;
174
+ } else {
175
+ throw new TypeError(`Invalid parameter schema: ${inspect(param)}`);
176
+ }
177
+ },
178
+ ),
179
+
180
+ createAsyncAssertion(
181
+ [
182
+ WrappedPromiseLikeSchema,
183
+ ['to fulfill with value satisfying', 'to resolve to value satisfying'],
184
+ z.union([z.string(), z.instanceof(RegExp), z.looseObject({})]),
185
+ ],
186
+ async (promise, param) => {
187
+ let value: unknown;
188
+ try {
189
+ value = await promise;
190
+ } catch (err) {
191
+ return {
192
+ actual: err,
193
+ expect: 'promise to not reject',
194
+ message: `Expected promise to not reject, but it rejected with ${inspect(
195
+ err,
196
+ )}`,
197
+ };
198
+ }
199
+ if (isString(param)) {
200
+ return z
201
+ .object({
202
+ message: z.coerce.string().pipe(z.literal(param)),
203
+ })
204
+ .or(z.coerce.string().pipe(z.literal(param)))
205
+ .safeParse(value).success;
206
+ } else if (isA(param, RegExp)) {
207
+ return z
208
+ .object({
209
+ message: z.coerce.string().regex(param),
210
+ })
211
+ .or(z.coerce.string().regex(param))
212
+ .safeParse(value).success;
213
+ } else if (isNonNullObject(param)) {
214
+ const schema = valueToSchema(param);
215
+ const result = schema.safeParse(value);
216
+ if (!result.success) {
217
+ return {
218
+ actual: value,
219
+ expected: param,
220
+ message: `Expected resolved value to satisfy schema ${inspect(
221
+ param,
222
+ )}, but it does not`,
223
+ };
224
+ }
225
+ return true;
226
+ } else {
227
+ throw new TypeError(`Invalid parameter schema: ${inspect(param)}`);
228
+ }
229
+ },
230
+ ),
231
+
232
+ createAsyncAssertion(
233
+ [
234
+ FunctionSchema,
235
+ ['to fulfill with value satisfying', 'to resolve to value satisfying'],
236
+ z.union([z.string(), z.instanceof(RegExp), z.looseObject({})]),
237
+ ],
238
+ async (subject, param) => {
239
+ let value: unknown;
240
+ try {
241
+ value = await subject();
242
+ } catch (err) {
243
+ return {
244
+ actual: err,
245
+ expect: 'function to not throw',
246
+ message: `Expected function to not throw, but it threw ${inspect(
247
+ err,
248
+ )}`,
249
+ };
250
+ }
251
+
252
+ if (isString(param)) {
253
+ return z
254
+ .object({
255
+ message: z.coerce.string().pipe(z.literal(param)),
256
+ })
257
+ .or(z.coerce.string().pipe(z.literal(param)))
258
+ .safeParse(value).success;
259
+ } else if (isA(param, RegExp)) {
260
+ return z
261
+ .object({
262
+ message: z.coerce.string().regex(param),
263
+ })
264
+ .or(z.coerce.string().regex(param))
265
+ .safeParse(value).success;
266
+ } else if (isNonNullObject(param)) {
267
+ const schema = valueToSchema(param);
268
+ const result = schema.safeParse(value);
269
+ if (!result.success) {
270
+ return {
271
+ actual: value,
272
+ expected: param,
273
+ message: `Expected resolved value to satisfy schema ${inspect(
274
+ param,
275
+ )}, but it does not`,
276
+ };
277
+ }
278
+ return true;
279
+ } else {
280
+ throw new TypeError(`Invalid parameter schema: ${inspect(param)}`);
281
+ }
282
+ },
283
+ ),
284
+ ] as const;
@@ -0,0 +1,2 @@
1
+ export { AsyncAssertions } from './async.js';
2
+ export { SyncAssertions } from './sync.js';
@@ -0,0 +1,111 @@
1
+ import { z } from 'zod/v4';
2
+
3
+ import { BupkisRegistry } from '../../metadata.js';
4
+ import {
5
+ AsyncFunctionSchema,
6
+ ClassSchema,
7
+ FalsySchema,
8
+ FunctionSchema,
9
+ PrimitiveSchema,
10
+ PropertyKeySchema,
11
+ StrongSetSchema,
12
+ TruthySchema,
13
+ } from '../../schema.js';
14
+ import { createAssertion } from '../create.js';
15
+
16
+ /**
17
+ * Basic synchronous assertions.
18
+ */
19
+ export const BasicAssertions = [
20
+ createAssertion(['to be a string'], z.string()),
21
+ createAssertion([['to be a number', 'to be finite']], z.number()),
22
+ createAssertion(['to be infinite'], z.literal([Infinity, -Infinity])),
23
+ createAssertion(['to be Infinity'], z.literal(Infinity)),
24
+ createAssertion(['to be -Infinity'], z.literal(-Infinity)),
25
+ createAssertion(
26
+ [['to be a boolean', 'to be boolean', 'to be a bool']],
27
+ z.boolean(),
28
+ ),
29
+ createAssertion(
30
+ [['to be positive', 'to be a positive number']],
31
+ z.number().positive(),
32
+ ),
33
+ createAssertion(
34
+ [['to be a positive integer', 'to be a positive int']],
35
+ z.number().int().positive(),
36
+ ),
37
+ createAssertion(
38
+ [['to be negative', 'to be a negative number']],
39
+ z.number().negative(),
40
+ ),
41
+ createAssertion(
42
+ [['to be a negative integer', 'to be a negative int']],
43
+ z.number().int().negative(),
44
+ ),
45
+ createAssertion(['to be true'], z.literal(true)),
46
+ createAssertion(['to be false'], z.literal(false)),
47
+ createAssertion([['to be a bigint', 'to be a BigInt']], z.bigint()),
48
+ createAssertion([['to be a symbol', 'to be a Symbol']], z.symbol()),
49
+ createAssertion(['to be a function'], FunctionSchema),
50
+ createAssertion(['to be an async function'], AsyncFunctionSchema),
51
+ createAssertion(['to be NaN'], z.nan()),
52
+ createAssertion(
53
+ [
54
+ [
55
+ 'to be an integer',
56
+ 'to be a safe integer',
57
+ 'to be an int',
58
+ 'to be a safe int',
59
+ ],
60
+ ],
61
+ z.number().int(),
62
+ ),
63
+ createAssertion(['to be null'], z.null()),
64
+ createAssertion(['to be undefined'], z.undefined()),
65
+ createAssertion([['to be an array', 'to be array']], z.array(z.any())),
66
+ createAssertion([['to be a date', 'to be a Date']], z.date()),
67
+ createAssertion([['to be a class', 'to be a constructor']], ClassSchema),
68
+ createAssertion(['to be a primitive'], PrimitiveSchema),
69
+
70
+ createAssertion(
71
+ [['to be a RegExp', 'to be a regex', 'to be a regexp']],
72
+ z.instanceof(RegExp),
73
+ ),
74
+ createAssertion([['to be truthy', 'to exist', 'to be ok']], TruthySchema),
75
+ createAssertion(['to be falsy'], FalsySchema),
76
+ createAssertion(
77
+ ['to be an object'],
78
+ z
79
+ .any()
80
+ .nonoptional()
81
+ .refine((value) => typeof value == 'object' && value !== null)
82
+ .describe(
83
+ 'Returns true for any non-null value where `typeof value` is `object`',
84
+ )
85
+ .register(BupkisRegistry, { name: 'Object' }),
86
+ ),
87
+ createAssertion(
88
+ [['to be a record', 'to be a plain object']],
89
+ z.record(PropertyKeySchema, z.unknown()),
90
+ ),
91
+ createAssertion([z.array(z.any()), 'to be empty'], z.array(z.any()).max(0)),
92
+ createAssertion(
93
+ [z.record(z.any(), z.unknown()), 'to be empty'],
94
+ z
95
+ .record(z.any(), z.unknown())
96
+ .refine((obj) => Reflect.ownKeys(obj).length === 0),
97
+ ),
98
+ createAssertion(['to be an Error'], z.instanceof(Error)),
99
+
100
+ // String emptiness assertions
101
+ createAssertion([z.string(), 'to be empty'], z.string().max(0)),
102
+ createAssertion([z.string(), 'to be non-empty'], z.string().min(1)),
103
+
104
+ // General existence/value checks
105
+ createAssertion(['to be defined'], z.unknown().nonoptional()),
106
+
107
+ // Basic collection type assertions
108
+ createAssertion(['to be a Set'], StrongSetSchema),
109
+ createAssertion(['to be a WeakMap'], z.instanceof(WeakMap)),
110
+ createAssertion(['to be a WeakSet'], z.instanceof(WeakSet)),
111
+ ] as const;
@@ -0,0 +1,108 @@
1
+ import { z } from 'zod/v4';
2
+
3
+ import { isNullOrNonObject } from '../../guards.js';
4
+ import { StrongMapSchema, StrongSetSchema } from '../../schema.js';
5
+ import { createAssertion } from '../create.js';
6
+
7
+ export const CollectionAssertions = [
8
+ // Map assertions (including WeakMap)
9
+ createAssertion(
10
+ [z.map(z.any(), z.any()), ['to contain', 'to include'], z.any()],
11
+ (subject, key) => subject.has(key),
12
+ ),
13
+ // Size-based assertions only for strong Maps (not WeakMaps)
14
+ createAssertion(
15
+ [StrongMapSchema, 'to have size', z.number()],
16
+ (subject, expectedSize) => subject.size === expectedSize,
17
+ ),
18
+ createAssertion(
19
+ [StrongMapSchema, 'to be empty'],
20
+ (subject) => subject.size === 0,
21
+ ),
22
+ // Set assertions (including WeakSet)
23
+ createAssertion(
24
+ [z.set(z.any()), ['to contain', 'to include'], z.any()],
25
+ (subject, value) => subject.has(value),
26
+ ),
27
+ // Size-based assertions only for strong Sets (not WeakSets)
28
+ createAssertion(
29
+ [StrongSetSchema, 'to have size', z.number()],
30
+ (subject, expectedSize) => subject.size === expectedSize,
31
+ ),
32
+ createAssertion(
33
+ [StrongSetSchema, 'to be empty'],
34
+ (subject) => subject.size === 0,
35
+ ),
36
+ // WeakMap specific assertions
37
+ createAssertion(
38
+ [z.instanceof(WeakMap), ['to contain', 'to include'], z.any()],
39
+ (subject, key) => {
40
+ // WeakMap.has only works with object keys
41
+ if (isNullOrNonObject(key)) {
42
+ return false;
43
+ }
44
+ return subject.has(key as WeakKey);
45
+ },
46
+ ),
47
+ // WeakSet specific assertions
48
+ createAssertion(
49
+ [z.instanceof(WeakSet), ['to contain', 'to include'], z.any()],
50
+ (subject, value) => {
51
+ // WeakSet.has only works with object values
52
+ if (isNullOrNonObject(value)) {
53
+ return false;
54
+ }
55
+ return subject.has(value as WeakKey);
56
+ },
57
+ ),
58
+ // Array assertions
59
+ createAssertion(
60
+ [z.array(z.any()), ['to contain', 'to include'], z.any()],
61
+ (subject, value) => subject.includes(value),
62
+ ),
63
+ createAssertion(
64
+ [z.array(z.any()), 'to have size', z.number()],
65
+ (subject, expectedSize) => subject.length === expectedSize,
66
+ ),
67
+ createAssertion(
68
+ [z.array(z.any()), 'to have length', z.number()],
69
+ (subject, expectedLength) => subject.length === expectedLength,
70
+ ),
71
+
72
+ // Array emptiness assertions
73
+ createAssertion([z.array(z.any()), 'to be non-empty'], (subject) => {
74
+ if (subject.length === 0) {
75
+ return {
76
+ actual: subject.length,
77
+ expected: 'non-empty array',
78
+ message: 'Expected array to be non-empty',
79
+ };
80
+ }
81
+ }),
82
+
83
+ // Object assertions
84
+ createAssertion(
85
+ [
86
+ z.looseObject({}),
87
+ ['to have keys', 'to have properties', 'to have props'],
88
+ z.tuple([z.string()], z.string()),
89
+ ],
90
+ (_, keys) =>
91
+ z.looseObject(
92
+ Object.fromEntries(keys.map((k) => [k, z.unknown().nonoptional()])),
93
+ ),
94
+ ),
95
+ createAssertion(
96
+ [z.looseObject({}), 'to have size', z.number().int().nonnegative()],
97
+ (subject, expectedSize) => {
98
+ const actual = Object.keys(subject).length;
99
+ if (actual !== expectedSize) {
100
+ return {
101
+ actual: actual,
102
+ expected: expectedSize,
103
+ message: `Expected object to have ${expectedSize} keys, but it has ${actual} keys`,
104
+ };
105
+ }
106
+ },
107
+ ),
108
+ ] as const;
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod/v4';
2
+
3
+ import { NullProtoObjectSchema, PropertyKeySchema } from '../../schema.js';
4
+ import { createAssertion } from '../create.js';
5
+
6
+ export const EsotericAssertions = [
7
+ createAssertion(['to have a null prototype'], NullProtoObjectSchema),
8
+ createAssertion(
9
+ [PropertyKeySchema, 'to be an enumerable property of', z.looseObject({})],
10
+ (subject, obj) =>
11
+ !!Object.getOwnPropertyDescriptor(obj, subject)?.enumerable,
12
+ ),
13
+ createAssertion(
14
+ ['to be sealed'],
15
+ z.any().refine((obj) => Object.isSealed(obj)),
16
+ ),
17
+ createAssertion(
18
+ ['to be frozen'],
19
+ z.any().refine((obj) => Object.isFrozen(obj)),
20
+ ),
21
+ createAssertion(
22
+ ['to be extensible'],
23
+ z.any().refine((obj) => Object.isExtensible(obj)),
24
+ ),
25
+ ] as const;