ai-tests 2.0.2 → 2.1.3

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,383 @@
1
+ /**
2
+ * Assertion utilities powered by Chai
3
+ *
4
+ * Exposes expect, should, and assert APIs via RPC.
5
+ * Uses Chai under the hood for battle-tested assertions.
6
+ */
7
+ import * as chai from 'chai';
8
+ import { RpcTarget } from 'cloudflare:workers';
9
+ // Initialize chai's should
10
+ chai.should();
11
+ /**
12
+ * Wrapper around Chai's expect that extends RpcTarget
13
+ * This allows the assertion chain to work over RPC with promise pipelining
14
+ */
15
+ export class Assertion extends RpcTarget {
16
+ // Using 'any' to avoid complex type gymnastics with Chai's chainable types
17
+ // (Deep, Nested, etc. don't match Chai.Assertion directly)
18
+ assertion;
19
+ constructor(value, message) {
20
+ super();
21
+ this.assertion = chai.expect(value, message);
22
+ }
23
+ // Chainable language chains
24
+ get to() { return this; }
25
+ get be() { return this; }
26
+ get been() { return this; }
27
+ get is() { return this; }
28
+ get that() { return this; }
29
+ get which() { return this; }
30
+ get and() { return this; }
31
+ get has() { return this; }
32
+ get have() { return this; }
33
+ get with() { return this; }
34
+ get at() { return this; }
35
+ get of() { return this; }
36
+ get same() { return this; }
37
+ get but() { return this; }
38
+ get does() { return this; }
39
+ get still() { return this; }
40
+ get also() { return this; }
41
+ // Negation
42
+ get not() {
43
+ this.assertion = this.assertion.not;
44
+ return this;
45
+ }
46
+ // Deep flag
47
+ get deep() {
48
+ this.assertion = this.assertion.deep;
49
+ return this;
50
+ }
51
+ // Nested flag
52
+ get nested() {
53
+ this.assertion = this.assertion.nested;
54
+ return this;
55
+ }
56
+ // Own flag
57
+ get own() {
58
+ this.assertion = this.assertion.own;
59
+ return this;
60
+ }
61
+ // Ordered flag
62
+ get ordered() {
63
+ this.assertion = this.assertion.ordered;
64
+ return this;
65
+ }
66
+ // Any flag
67
+ get any() {
68
+ this.assertion = this.assertion.any;
69
+ return this;
70
+ }
71
+ // All flag
72
+ get all() {
73
+ this.assertion = this.assertion.all;
74
+ return this;
75
+ }
76
+ // Length chain
77
+ get length() {
78
+ this.assertion = this.assertion.length;
79
+ return this;
80
+ }
81
+ // Type assertions
82
+ get ok() { this.assertion.ok; return this; }
83
+ get true() { this.assertion.true; return this; }
84
+ get false() { this.assertion.false; return this; }
85
+ get null() { this.assertion.null; return this; }
86
+ get undefined() { this.assertion.undefined; return this; }
87
+ get NaN() { this.assertion.NaN; return this; }
88
+ get exist() { this.assertion.exist; return this; }
89
+ get empty() { this.assertion.empty; return this; }
90
+ get arguments() { this.assertion.arguments; return this; }
91
+ // Value assertions
92
+ equal(value, message) {
93
+ this.assertion.equal(value, message);
94
+ return this;
95
+ }
96
+ equals(value, message) {
97
+ return this.equal(value, message);
98
+ }
99
+ eq(value, message) {
100
+ return this.equal(value, message);
101
+ }
102
+ eql(value, message) {
103
+ this.assertion.eql(value, message);
104
+ return this;
105
+ }
106
+ eqls(value, message) {
107
+ return this.eql(value, message);
108
+ }
109
+ above(value, message) {
110
+ this.assertion.above(value, message);
111
+ return this;
112
+ }
113
+ gt(value, message) {
114
+ return this.above(value, message);
115
+ }
116
+ greaterThan(value, message) {
117
+ return this.above(value, message);
118
+ }
119
+ least(value, message) {
120
+ this.assertion.least(value, message);
121
+ return this;
122
+ }
123
+ gte(value, message) {
124
+ return this.least(value, message);
125
+ }
126
+ greaterThanOrEqual(value, message) {
127
+ return this.least(value, message);
128
+ }
129
+ below(value, message) {
130
+ this.assertion.below(value, message);
131
+ return this;
132
+ }
133
+ lt(value, message) {
134
+ return this.below(value, message);
135
+ }
136
+ lessThan(value, message) {
137
+ return this.below(value, message);
138
+ }
139
+ most(value, message) {
140
+ this.assertion.most(value, message);
141
+ return this;
142
+ }
143
+ lte(value, message) {
144
+ return this.most(value, message);
145
+ }
146
+ lessThanOrEqual(value, message) {
147
+ return this.most(value, message);
148
+ }
149
+ within(start, finish, message) {
150
+ this.assertion.within(start, finish, message);
151
+ return this;
152
+ }
153
+ instanceof(constructor, message) {
154
+ this.assertion.instanceof(constructor, message);
155
+ return this;
156
+ }
157
+ instanceOf(constructor, message) {
158
+ return this.instanceof(constructor, message);
159
+ }
160
+ property(name, value, message) {
161
+ if (arguments.length > 1) {
162
+ this.assertion.property(name, value, message);
163
+ }
164
+ else {
165
+ this.assertion.property(name);
166
+ }
167
+ return this;
168
+ }
169
+ ownProperty(name, message) {
170
+ this.assertion.ownProperty(name, message);
171
+ return this;
172
+ }
173
+ haveOwnProperty(name, message) {
174
+ return this.ownProperty(name, message);
175
+ }
176
+ ownPropertyDescriptor(name, descriptor, message) {
177
+ this.assertion.ownPropertyDescriptor(name, descriptor, message);
178
+ return this;
179
+ }
180
+ lengthOf(n, message) {
181
+ this.assertion.lengthOf(n, message);
182
+ return this;
183
+ }
184
+ match(re, message) {
185
+ this.assertion.match(re, message);
186
+ return this;
187
+ }
188
+ matches(re, message) {
189
+ return this.match(re, message);
190
+ }
191
+ string(str, message) {
192
+ this.assertion.string(str, message);
193
+ return this;
194
+ }
195
+ keys(...keys) {
196
+ this.assertion.keys(...keys);
197
+ return this;
198
+ }
199
+ key(...keys) {
200
+ return this.keys(...keys);
201
+ }
202
+ throw(errorLike, errMsgMatcher, message) {
203
+ this.assertion.throw(errorLike, errMsgMatcher, message);
204
+ return this;
205
+ }
206
+ throws(errorLike, errMsgMatcher, message) {
207
+ return this.throw(errorLike, errMsgMatcher, message);
208
+ }
209
+ Throw(errorLike, errMsgMatcher, message) {
210
+ return this.throw(errorLike, errMsgMatcher, message);
211
+ }
212
+ respondTo(method, message) {
213
+ this.assertion.respondTo(method, message);
214
+ return this;
215
+ }
216
+ respondsTo(method, message) {
217
+ return this.respondTo(method, message);
218
+ }
219
+ satisfy(matcher, message) {
220
+ this.assertion.satisfy(matcher, message);
221
+ return this;
222
+ }
223
+ satisfies(matcher, message) {
224
+ return this.satisfy(matcher, message);
225
+ }
226
+ closeTo(expected, delta, message) {
227
+ this.assertion.closeTo(expected, delta, message);
228
+ return this;
229
+ }
230
+ approximately(expected, delta, message) {
231
+ return this.closeTo(expected, delta, message);
232
+ }
233
+ members(set, message) {
234
+ this.assertion.members(set, message);
235
+ return this;
236
+ }
237
+ oneOf(list, message) {
238
+ this.assertion.oneOf(list, message);
239
+ return this;
240
+ }
241
+ include(value, message) {
242
+ this.assertion.include(value, message);
243
+ return this;
244
+ }
245
+ includes(value, message) {
246
+ return this.include(value, message);
247
+ }
248
+ contain(value, message) {
249
+ return this.include(value, message);
250
+ }
251
+ contains(value, message) {
252
+ return this.include(value, message);
253
+ }
254
+ a(type, message) {
255
+ this.assertion.a(type, message);
256
+ return this;
257
+ }
258
+ an(type, message) {
259
+ return this.a(type, message);
260
+ }
261
+ // Vitest-compatible aliases
262
+ toBe(value) {
263
+ this.assertion.equal(value);
264
+ return this;
265
+ }
266
+ toEqual(value) {
267
+ this.assertion.deep.equal(value);
268
+ return this;
269
+ }
270
+ toStrictEqual(value) {
271
+ this.assertion.deep.equal(value);
272
+ return this;
273
+ }
274
+ toBeTruthy() {
275
+ this.assertion.ok;
276
+ return this;
277
+ }
278
+ toBeFalsy() {
279
+ this.assertion.not.ok;
280
+ return this;
281
+ }
282
+ toBeNull() {
283
+ this.assertion.null;
284
+ return this;
285
+ }
286
+ toBeUndefined() {
287
+ this.assertion.undefined;
288
+ return this;
289
+ }
290
+ toBeDefined() {
291
+ this.assertion.not.undefined;
292
+ return this;
293
+ }
294
+ toBeNaN() {
295
+ this.assertion.NaN;
296
+ return this;
297
+ }
298
+ toContain(value) {
299
+ this.assertion.include(value);
300
+ return this;
301
+ }
302
+ toHaveLength(length) {
303
+ this.assertion.lengthOf(length);
304
+ return this;
305
+ }
306
+ toHaveProperty(path, value) {
307
+ if (arguments.length > 1) {
308
+ this.assertion.nested.property(path, value);
309
+ }
310
+ else {
311
+ this.assertion.nested.property(path);
312
+ }
313
+ return this;
314
+ }
315
+ toMatch(pattern) {
316
+ if (typeof pattern === 'string') {
317
+ this.assertion.include(pattern);
318
+ }
319
+ else {
320
+ this.assertion.match(pattern);
321
+ }
322
+ return this;
323
+ }
324
+ toMatchObject(obj) {
325
+ this.assertion.deep.include(obj);
326
+ return this;
327
+ }
328
+ toThrow(expected) {
329
+ if (expected) {
330
+ this.assertion.throw(expected);
331
+ }
332
+ else {
333
+ this.assertion.throw();
334
+ }
335
+ return this;
336
+ }
337
+ toBeGreaterThan(n) {
338
+ this.assertion.above(n);
339
+ return this;
340
+ }
341
+ toBeLessThan(n) {
342
+ this.assertion.below(n);
343
+ return this;
344
+ }
345
+ toBeGreaterThanOrEqual(n) {
346
+ this.assertion.least(n);
347
+ return this;
348
+ }
349
+ toBeLessThanOrEqual(n) {
350
+ this.assertion.most(n);
351
+ return this;
352
+ }
353
+ toBeCloseTo(n, digits = 2) {
354
+ const delta = Math.pow(10, -digits) / 2;
355
+ this.assertion.closeTo(n, delta);
356
+ return this;
357
+ }
358
+ toBeInstanceOf(cls) {
359
+ this.assertion.instanceof(cls);
360
+ return this;
361
+ }
362
+ toBeTypeOf(type) {
363
+ this.assertion.a(type);
364
+ return this;
365
+ }
366
+ }
367
+ /**
368
+ * Assert API - TDD style assertions
369
+ */
370
+ export const assert = chai.assert;
371
+ /**
372
+ * Create an expect assertion
373
+ */
374
+ export function expect(value, message) {
375
+ return new Assertion(value, message);
376
+ }
377
+ /**
378
+ * Create a should-style assertion
379
+ * Since we can't modify Object.prototype over RPC, this takes a value
380
+ */
381
+ export function should(value) {
382
+ return new Assertion(value);
383
+ }
package/src/assertions.ts CHANGED
@@ -11,14 +11,65 @@ import { RpcTarget } from 'cloudflare:workers'
11
11
  // Initialize chai's should
12
12
  chai.should()
13
13
 
14
+ /**
15
+ * Type for constructor functions that can be used with instanceof assertions.
16
+ * Matches any class or function that can construct instances.
17
+ */
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ type Constructor<T = unknown> = new (...args: any[]) => T
20
+
21
+ /**
22
+ * Type for error-like values that can be used with throw assertions.
23
+ * Chai's throw() accepts:
24
+ * - Error constructor (e.g., TypeError, RangeError)
25
+ * - Error instance (e.g., new Error('message'))
26
+ * - String for message matching
27
+ * - RegExp for pattern matching
28
+ */
29
+ type ThrowableMatch = string | RegExp | Error | Constructor<Error>
30
+
31
+ /**
32
+ * Internal type for Chai assertion chain.
33
+ *
34
+ * @remarks
35
+ * **Why we use Chai.Assertion here:**
36
+ *
37
+ * Chai's fluent API uses a chainable pattern where each flag (.not, .deep, .nested, etc.)
38
+ * returns a different TypeScript interface:
39
+ * - Chai.Assertion: Base type from expect()
40
+ * - Chai.Deep: Returned after .deep (has subset of properties)
41
+ * - Chai.Nested, Chai.Own, etc.: Other flag-specific types
42
+ *
43
+ * These types form a complex hierarchy where not all methods exist on all types.
44
+ * For example, Chai.Deep has .equal() but not .ok, while Chai.KeyFilter has
45
+ * .keys() but not .equal().
46
+ *
47
+ * Our wrapper class stores the current assertion state and calls methods on it.
48
+ * At runtime, all these methods exist because Chai's actual implementation always
49
+ * has them - the type restrictions are for API guidance, not runtime behavior.
50
+ *
51
+ * We use Chai.Assertion (the most complete type) because:
52
+ * 1. It has all the methods we need to call
53
+ * 2. The assignment from flag types to Chai.Assertion is unsound in strict TS,
54
+ * but at runtime these objects have all the methods we need
55
+ * 3. We wrap the result in our own Assertion class, so the internal type
56
+ * doesn't leak to consumers
57
+ *
58
+ * The type assertions in the flag getters (.deep, .nested, etc.) are intentional
59
+ * and documented. They bridge Chai's complex type hierarchy to our simpler wrapper.
60
+ */
61
+ type ChaiAssertionChain = Chai.Assertion
62
+
14
63
  /**
15
64
  * Wrapper around Chai's expect that extends RpcTarget
16
65
  * This allows the assertion chain to work over RPC with promise pipelining
17
66
  */
18
67
  export class Assertion extends RpcTarget {
19
- // Using 'any' to avoid complex type gymnastics with Chai's chainable types
20
- // (Deep, Nested, etc. don't match Chai.Assertion directly)
21
- private assertion: any
68
+ /**
69
+ * Internal Chai assertion chain.
70
+ * @see ChaiAssertionChain for explanation of the type choice.
71
+ */
72
+ private assertion: ChaiAssertionChain
22
73
 
23
74
  constructor(value: unknown, message?: string) {
24
75
  super()
@@ -44,51 +95,79 @@ export class Assertion extends RpcTarget {
44
95
  get still() { return this }
45
96
  get also() { return this }
46
97
 
47
- // Negation
98
+ /**
99
+ * Negation flag - inverts the assertion.
100
+ * @remarks Type cast needed: Chai's .not returns Chai.Assertion, which matches our type.
101
+ */
48
102
  get not(): Assertion {
49
103
  this.assertion = this.assertion.not
50
104
  return this
51
105
  }
52
106
 
53
- // Deep flag
107
+ /**
108
+ * Deep flag - enables deep equality comparisons.
109
+ * @remarks Type cast needed: Chai's .deep returns Chai.Deep, but at runtime
110
+ * it has all the methods we need. We cast to maintain our wrapper's type.
111
+ */
54
112
  get deep(): Assertion {
55
- this.assertion = this.assertion.deep
113
+ this.assertion = this.assertion.deep as ChaiAssertionChain
56
114
  return this
57
115
  }
58
116
 
59
- // Nested flag
117
+ /**
118
+ * Nested flag - enables nested property access with dot notation.
119
+ * @remarks Type cast needed: Chai's .nested returns Chai.Nested, cast to our chain type.
120
+ */
60
121
  get nested(): Assertion {
61
- this.assertion = this.assertion.nested
122
+ this.assertion = this.assertion.nested as ChaiAssertionChain
62
123
  return this
63
124
  }
64
125
 
65
- // Own flag
126
+ /**
127
+ * Own flag - only checks own properties, not inherited.
128
+ * @remarks Type cast needed: Chai's .own returns Chai.Own, cast to our chain type.
129
+ */
66
130
  get own(): Assertion {
67
- this.assertion = this.assertion.own
131
+ this.assertion = this.assertion.own as ChaiAssertionChain
68
132
  return this
69
133
  }
70
134
 
71
- // Ordered flag
135
+ /**
136
+ * Ordered flag - requires members to be in order.
137
+ * @remarks Type cast needed: Chai's .ordered returns Chai.Ordered, cast to our chain type.
138
+ */
72
139
  get ordered(): Assertion {
73
- this.assertion = this.assertion.ordered
140
+ this.assertion = this.assertion.ordered as ChaiAssertionChain
74
141
  return this
75
142
  }
76
143
 
77
- // Any flag
144
+ /**
145
+ * Any flag - requires at least one key match.
146
+ * @remarks Type cast needed: Chai's .any returns Chai.KeyFilter, cast to our chain type.
147
+ */
78
148
  get any(): Assertion {
79
- this.assertion = this.assertion.any
149
+ this.assertion = this.assertion.any as ChaiAssertionChain
80
150
  return this
81
151
  }
82
152
 
83
- // All flag
153
+ /**
154
+ * All flag - requires all keys to match.
155
+ * @remarks Type cast needed: Chai's .all returns Chai.KeyFilter, cast to our chain type.
156
+ */
84
157
  get all(): Assertion {
85
- this.assertion = this.assertion.all
158
+ this.assertion = this.assertion.all as ChaiAssertionChain
86
159
  return this
87
160
  }
88
161
 
89
- // Length chain
162
+ /**
163
+ * Length chain - for asserting on length property.
164
+ * @remarks Type cast needed: Chai's .length returns Chai.Length which is quite different
165
+ * from Chai.Assertion (it's also callable). We cast through unknown because Length
166
+ * doesn't directly overlap with Assertion in TypeScript's structural type system,
167
+ * but at runtime the object has all methods we need.
168
+ */
90
169
  get length(): Assertion {
91
- this.assertion = this.assertion.length
170
+ this.assertion = this.assertion.length as unknown as ChaiAssertionChain
92
171
  return this
93
172
  }
94
173
 
@@ -183,12 +262,12 @@ export class Assertion extends RpcTarget {
183
262
  return this
184
263
  }
185
264
 
186
- instanceof(constructor: unknown, message?: string) {
187
- this.assertion.instanceof(constructor as any, message)
265
+ instanceof(constructor: Constructor, message?: string) {
266
+ this.assertion.instanceof(constructor, message)
188
267
  return this
189
268
  }
190
269
 
191
- instanceOf(constructor: unknown, message?: string) {
270
+ instanceOf(constructor: Constructor, message?: string) {
192
271
  return this.instanceof(constructor, message)
193
272
  }
194
273
 
@@ -211,7 +290,11 @@ export class Assertion extends RpcTarget {
211
290
  }
212
291
 
213
292
  ownPropertyDescriptor(name: string, descriptor?: PropertyDescriptor, message?: string) {
214
- this.assertion.ownPropertyDescriptor(name, descriptor, message)
293
+ if (descriptor !== undefined) {
294
+ this.assertion.ownPropertyDescriptor(name, descriptor, message)
295
+ } else {
296
+ this.assertion.ownPropertyDescriptor(name, message)
297
+ }
215
298
  return this
216
299
  }
217
300
 
@@ -243,16 +326,37 @@ export class Assertion extends RpcTarget {
243
326
  return this.keys(...keys)
244
327
  }
245
328
 
246
- throw(errorLike?: unknown, errMsgMatcher?: string | RegExp, message?: string) {
247
- this.assertion.throw(errorLike as any, errMsgMatcher, message)
329
+ /**
330
+ * Asserts that the function throws an error.
331
+ *
332
+ * @param errorLike - Error constructor, instance, string, or RegExp to match
333
+ * @param errMsgMatcher - String or RegExp to match error message (when errorLike is constructor/instance)
334
+ * @param message - Custom assertion message
335
+ *
336
+ * @remarks
337
+ * Chai's throw() has two overloads that TypeScript can't resolve with our union type.
338
+ * We use runtime type checking to call the appropriate overload, then use a type
339
+ * assertion to satisfy TypeScript. This is safe because Chai accepts all these
340
+ * combinations at runtime.
341
+ */
342
+ throw(errorLike?: ThrowableMatch, errMsgMatcher?: string | RegExp, message?: string) {
343
+ if (errorLike === undefined) {
344
+ this.assertion.throw()
345
+ } else if (typeof errorLike === 'string' || errorLike instanceof RegExp) {
346
+ // First overload: (expected?: string | RegExp, message?: string)
347
+ this.assertion.throw(errorLike, errMsgMatcher as string | undefined)
348
+ } else {
349
+ // Second overload: (constructor: Error | Function, expected?: string | RegExp, message?: string)
350
+ this.assertion.throw(errorLike as Error | Function, errMsgMatcher, message)
351
+ }
248
352
  return this
249
353
  }
250
354
 
251
- throws(errorLike?: unknown, errMsgMatcher?: string | RegExp, message?: string) {
355
+ throws(errorLike?: ThrowableMatch, errMsgMatcher?: string | RegExp, message?: string) {
252
356
  return this.throw(errorLike, errMsgMatcher, message)
253
357
  }
254
358
 
255
- Throw(errorLike?: unknown, errMsgMatcher?: string | RegExp, message?: string) {
359
+ Throw(errorLike?: ThrowableMatch, errMsgMatcher?: string | RegExp, message?: string) {
256
360
  return this.throw(errorLike, errMsgMatcher, message)
257
361
  }
258
362
 
@@ -398,11 +502,18 @@ export class Assertion extends RpcTarget {
398
502
  return this
399
503
  }
400
504
 
401
- toThrow(expected?: string | RegExp | Error) {
402
- if (expected) {
403
- this.assertion.throw(expected as any)
404
- } else {
505
+ /**
506
+ * Vitest-compatible: Asserts that the function throws.
507
+ * @remarks Uses same runtime type checking as throw() to handle Chai's overloads.
508
+ */
509
+ toThrow(expected?: ThrowableMatch) {
510
+ if (expected === undefined) {
405
511
  this.assertion.throw()
512
+ } else if (typeof expected === 'string' || expected instanceof RegExp) {
513
+ this.assertion.throw(expected)
514
+ } else {
515
+ // Error instance or constructor
516
+ this.assertion.throw(expected as Error | Function)
406
517
  }
407
518
  return this
408
519
  }
@@ -433,8 +544,8 @@ export class Assertion extends RpcTarget {
433
544
  return this
434
545
  }
435
546
 
436
- toBeInstanceOf(cls: unknown) {
437
- this.assertion.instanceof(cls as any)
547
+ toBeInstanceOf(cls: Constructor) {
548
+ this.assertion.instanceof(cls)
438
549
  return this
439
550
  }
440
551