@plugjs/expect5 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.
Files changed (90) hide show
  1. package/README.md +7 -0
  2. package/dist/cli.d.mts +2 -0
  3. package/dist/cli.mjs +96 -0
  4. package/dist/cli.mjs.map +6 -0
  5. package/dist/execution/executable.cjs +299 -0
  6. package/dist/execution/executable.cjs.map +6 -0
  7. package/dist/execution/executable.d.ts +87 -0
  8. package/dist/execution/executable.mjs +260 -0
  9. package/dist/execution/executable.mjs.map +6 -0
  10. package/dist/execution/executor.cjs +125 -0
  11. package/dist/execution/executor.cjs.map +6 -0
  12. package/dist/execution/executor.d.ts +35 -0
  13. package/dist/execution/executor.mjs +90 -0
  14. package/dist/execution/executor.mjs.map +6 -0
  15. package/dist/execution/setup.cjs +127 -0
  16. package/dist/execution/setup.cjs.map +6 -0
  17. package/dist/execution/setup.d.ts +31 -0
  18. package/dist/execution/setup.mjs +87 -0
  19. package/dist/execution/setup.mjs.map +6 -0
  20. package/dist/expectation/basic.cjs +216 -0
  21. package/dist/expectation/basic.cjs.map +6 -0
  22. package/dist/expectation/basic.d.ts +47 -0
  23. package/dist/expectation/basic.mjs +177 -0
  24. package/dist/expectation/basic.mjs.map +6 -0
  25. package/dist/expectation/diff.cjs +253 -0
  26. package/dist/expectation/diff.cjs.map +6 -0
  27. package/dist/expectation/diff.d.ts +27 -0
  28. package/dist/expectation/diff.mjs +228 -0
  29. package/dist/expectation/diff.mjs.map +6 -0
  30. package/dist/expectation/expect.cjs +211 -0
  31. package/dist/expectation/expect.cjs.map +6 -0
  32. package/dist/expectation/expect.d.ts +140 -0
  33. package/dist/expectation/expect.mjs +219 -0
  34. package/dist/expectation/expect.mjs.map +6 -0
  35. package/dist/expectation/include.cjs +187 -0
  36. package/dist/expectation/include.cjs.map +6 -0
  37. package/dist/expectation/include.d.ts +10 -0
  38. package/dist/expectation/include.mjs +158 -0
  39. package/dist/expectation/include.mjs.map +6 -0
  40. package/dist/expectation/print.cjs +281 -0
  41. package/dist/expectation/print.cjs.map +6 -0
  42. package/dist/expectation/print.d.ts +4 -0
  43. package/dist/expectation/print.mjs +256 -0
  44. package/dist/expectation/print.mjs.map +6 -0
  45. package/dist/expectation/throwing.cjs +58 -0
  46. package/dist/expectation/throwing.cjs.map +6 -0
  47. package/dist/expectation/throwing.d.ts +8 -0
  48. package/dist/expectation/throwing.mjs +32 -0
  49. package/dist/expectation/throwing.mjs.map +6 -0
  50. package/dist/expectation/types.cjs +212 -0
  51. package/dist/expectation/types.cjs.map +6 -0
  52. package/dist/expectation/types.d.ts +57 -0
  53. package/dist/expectation/types.mjs +178 -0
  54. package/dist/expectation/types.mjs.map +6 -0
  55. package/dist/expectation/void.cjs +111 -0
  56. package/dist/expectation/void.cjs.map +6 -0
  57. package/dist/expectation/void.d.ts +39 -0
  58. package/dist/expectation/void.mjs +77 -0
  59. package/dist/expectation/void.mjs.map +6 -0
  60. package/dist/globals.cjs +2 -0
  61. package/dist/globals.cjs.map +6 -0
  62. package/dist/globals.d.ts +23 -0
  63. package/dist/globals.mjs +1 -0
  64. package/dist/globals.mjs.map +6 -0
  65. package/dist/index.cjs +66 -0
  66. package/dist/index.cjs.map +6 -0
  67. package/dist/index.d.ts +29 -0
  68. package/dist/index.mjs +41 -0
  69. package/dist/index.mjs.map +6 -0
  70. package/dist/test.cjs +229 -0
  71. package/dist/test.cjs.map +6 -0
  72. package/dist/test.d.ts +9 -0
  73. package/dist/test.mjs +194 -0
  74. package/dist/test.mjs.map +6 -0
  75. package/package.json +57 -0
  76. package/src/cli.mts +122 -0
  77. package/src/execution/executable.ts +364 -0
  78. package/src/execution/executor.ts +146 -0
  79. package/src/execution/setup.ts +108 -0
  80. package/src/expectation/basic.ts +209 -0
  81. package/src/expectation/diff.ts +445 -0
  82. package/src/expectation/expect.ts +401 -0
  83. package/src/expectation/include.ts +184 -0
  84. package/src/expectation/print.ts +386 -0
  85. package/src/expectation/throwing.ts +45 -0
  86. package/src/expectation/types.ts +263 -0
  87. package/src/expectation/void.ts +80 -0
  88. package/src/globals.ts +30 -0
  89. package/src/index.ts +54 -0
  90. package/src/test.ts +239 -0
@@ -0,0 +1,209 @@
1
+ import { diff } from './diff'
2
+ import { assertType, ExpectationError, isType, prefixType, stringifyConstructor, stringifyValue } from './types'
3
+
4
+ import type { Constructor, TypeName, StringMatcher } from './types'
5
+ import type { Expectation, Expectations } from './expect'
6
+
7
+ export class ToBeA implements Expectation {
8
+ expect(context: Expectations, negative: boolean, type: TypeName): void {
9
+ const match = isType(context, type)
10
+ if (match !== negative) return
11
+ throw new ExpectationError(context, negative, `to be ${prefixType(type)}`)
12
+ }
13
+ }
14
+
15
+ export class ToBeCloseTo implements Expectation {
16
+ expect(
17
+ context: Expectations,
18
+ negative: boolean,
19
+ ...[ value, delta ]:
20
+ | [ value: number, delta: number ]
21
+ | [ value: bigint, delta: bigint ]
22
+ ): void {
23
+ const min = (value as number) - (delta as number)
24
+ const max = (value as number) + (delta as number)
25
+ context.negated(negative).toBeWithinRange(min, max)
26
+ }
27
+ }
28
+
29
+ export class ToBeError implements Expectation {
30
+ expect(
31
+ context: Expectations,
32
+ negative: boolean,
33
+ ...args:
34
+ | []
35
+ | [ message: StringMatcher ]
36
+ | [ constructor: Constructor<Error> ]
37
+ | [ constructor: Constructor<Error>, message: StringMatcher ]
38
+ ): void {
39
+ const [ constructor, message ] =
40
+ typeof args[0] === 'function' ?
41
+ [ args[0], args[1] ] :
42
+ [ Error, args[0] ]
43
+
44
+ context.negated(negative).toBeInstanceOf(constructor)
45
+ if (negative || (message === undefined)) return // if "not.toBeError" ignore the message
46
+
47
+ context.toHaveProperty('message', (assert) => {
48
+ assertType(assert, 'string')
49
+ if (typeof message === 'string') assert.toStrictlyEqual(message)
50
+ else assert.toMatch(message)
51
+ })
52
+ }
53
+ }
54
+
55
+ export class ToBeGreaterThan implements Expectation {
56
+ expect(context: Expectations, negative: boolean, value: number | bigint): void {
57
+ assertType(context, typeof value as 'number' | 'bigint')
58
+ if ((context.value > value) !== negative) return
59
+ throw new ExpectationError(context, negative, `to be greater than ${stringifyValue(value)}`)
60
+ }
61
+ }
62
+
63
+ export class ToBeGreaterThanOrEqual implements Expectation {
64
+ expect(context: Expectations, negative: boolean, value: number | bigint): void {
65
+ assertType(context, typeof value as 'number' | 'bigint')
66
+ if ((context.value >= value) !== negative) return
67
+ throw new ExpectationError(context, negative, `to be greater than or equal to ${stringifyValue(value)}`)
68
+ }
69
+ }
70
+
71
+ export class ToBeInstanceOf implements Expectation {
72
+ expect(context: Expectations, negative: boolean, value: Constructor): void {
73
+ const match = context.value instanceof value
74
+ if (match !== negative) return
75
+ throw new ExpectationError(context, negative, `to be an instance of ${stringifyConstructor(value)}`)
76
+ }
77
+ }
78
+
79
+ export class ToBeLessThan implements Expectation {
80
+ expect(context: Expectations, negative: boolean, value: number | bigint): void {
81
+ assertType(context, typeof value as 'number' | 'bigint')
82
+ if ((context.value < value) !== negative) return
83
+ throw new ExpectationError(context, negative, `to be less than ${stringifyValue(value)}`)
84
+ }
85
+ }
86
+
87
+ export class ToBeLessThanOrEqual implements Expectation {
88
+ expect(context: Expectations, negative: boolean, value: number | bigint): void {
89
+ assertType(context, typeof value as 'number' | 'bigint')
90
+ if ((context.value <= value) !== negative) return
91
+ throw new ExpectationError(context, negative, `to be less than or equal to ${stringifyValue(value)}`)
92
+ }
93
+ }
94
+
95
+ export class ToBeWithinRange implements Expectation {
96
+ expect(
97
+ context: Expectations,
98
+ negative: boolean,
99
+ ...[ min, max ]:
100
+ | [ min: number, max: number ]
101
+ | [ min: bigint, max: bigint ]
102
+ ): void {
103
+ if (max < min) {
104
+ const num = max
105
+ max = min
106
+ min = num
107
+ }
108
+
109
+ assertType(context, typeof min as 'number' | 'bigint')
110
+ if (((context.value >= min) && (context.value <= max)) !== negative) return
111
+ throw new ExpectationError(context, negative, `to be within ${stringifyValue(min)}...${stringifyValue(max)}`)
112
+ }
113
+ }
114
+
115
+ export class ToEqual implements Expectation {
116
+ expect(context: Expectations, negative: boolean, expected: any): void {
117
+ const result = diff(context.value, expected)
118
+ if (result.diff === negative) return
119
+ throw new ExpectationError(context, negative, `to equal ${stringifyValue(expected)}`, result)
120
+ }
121
+ }
122
+
123
+ export class ToHaveLength implements Expectation {
124
+ expect(context: Expectations, negative: boolean, length: number): void {
125
+ context.toBeDefined()
126
+
127
+ const actualLength = (context.value as any).length
128
+ if (typeof actualLength !== 'number') {
129
+ throw new ExpectationError(context, false, 'to have a numeric "length" property')
130
+ }
131
+
132
+ if ((actualLength === length) === negative) {
133
+ throw new ExpectationError(context, negative, `to have length ${stringifyValue(length)}`)
134
+ }
135
+ }
136
+ }
137
+
138
+ export class ToHaveProperty implements Expectation {
139
+ expect(
140
+ context: Expectations,
141
+ negative: boolean,
142
+ prop: string | number | symbol,
143
+ assert?: (propertyExpectations: Expectations) => void,
144
+ ): void {
145
+ context.toBeDefined()
146
+
147
+ const match = (context.value as any)[prop] !== undefined
148
+ if (match === negative) {
149
+ throw new ExpectationError(context, negative, `to have property "${String(prop)}"`)
150
+ } else if (match && assert) {
151
+ try {
152
+ assert(context.forProperty(prop))
153
+ } catch (error) {
154
+ // any caught error difference gets remapped as a property diff
155
+ if ((error instanceof ExpectationError) && (error.diff)) {
156
+ error.diff = {
157
+ diff: true,
158
+ value: context.value,
159
+ props: { [prop]: error.diff },
160
+ }
161
+ }
162
+
163
+ // re-throw
164
+ throw error
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ export class ToHaveSize implements Expectation {
171
+ expect(context: Expectations, negative: boolean, size: number): void {
172
+ context.toBeDefined()
173
+
174
+ const actualSize = (context.value as any).size
175
+ if (typeof actualSize !== 'number') {
176
+ throw new ExpectationError(context, false, 'to have a numeric "size" property')
177
+ }
178
+
179
+ if ((actualSize === size) === negative) {
180
+ throw new ExpectationError(context, negative, `to have size ${stringifyValue(size)}`)
181
+ }
182
+ }
183
+ }
184
+
185
+ export class ToMatch implements Expectation {
186
+ expect(
187
+ context: Expectations,
188
+ negative: boolean,
189
+ expr: StringMatcher,
190
+ ): void {
191
+ assertType(context, 'string')
192
+
193
+ const match = !! context.value.match(expr as string | RegExp) // meh, overloads
194
+ if (match !== negative) return
195
+
196
+ throw new ExpectationError(context, negative, `to match ${stringifyValue(expr)}`)
197
+ }
198
+ }
199
+
200
+ export class ToStrictlyEqual implements Expectation {
201
+ expect(context: Expectations, negative: boolean, expected: any): void {
202
+ const value = context.value
203
+ const match = value === expected
204
+ if (match !== negative) return
205
+
206
+ const diff = negative ? undefined : { diff: true, value, expected }
207
+ throw new ExpectationError(context, negative, `to strictly equal ${stringifyValue(expected)}`, diff)
208
+ }
209
+ }
@@ -0,0 +1,445 @@
1
+ /* eslint-disable no-fallthrough */
2
+ import { ExpectationError, isMatcher, stringifyConstructor, stringifyValue } from './types'
3
+
4
+ import type { Constructor } from './types'
5
+
6
+ export interface BaseDiff {
7
+ diff: boolean,
8
+ error?: string,
9
+ }
10
+
11
+ export interface ValueDiff extends BaseDiff {
12
+ value: any,
13
+ }
14
+
15
+ export interface ExpectedDiff extends ValueDiff {
16
+ diff: true,
17
+ expected: any,
18
+ }
19
+
20
+ export interface ExtraValueDiff extends BaseDiff {
21
+ diff: true,
22
+ extra: any,
23
+ }
24
+
25
+ export interface MissingValueDiff extends BaseDiff {
26
+ diff: true,
27
+ missing: any,
28
+ }
29
+
30
+ export interface ObjectDiff extends ValueDiff {
31
+ diff: boolean,
32
+ props?: Record<string, Diff>,
33
+ values?: Diff[],
34
+ mappings?: [ any, Diff ][]
35
+ }
36
+
37
+ export type Diff =
38
+ | ValueDiff
39
+ | ExpectedDiff
40
+ | ObjectDiff
41
+ | ExtraValueDiff
42
+ | MissingValueDiff
43
+
44
+ /* ========================================================================== *
45
+ * IMPLEMENTATION INTERNALS *
46
+ * ========================================================================== */
47
+
48
+ type Binary = Buffer | Uint8Array | ArrayBuffer | SharedArrayBuffer
49
+ type BoxedPrimitive = Boolean | String | Number
50
+ type Remarks = { actualMemos: any[], expectedMemos: any[] }
51
+
52
+ /* ========================================================================== */
53
+
54
+ function errorDiff(value: any, message: string): ValueDiff {
55
+ const error = `Expected ${stringifyValue(value)} ${message}`
56
+ return { diff: true, value, error }
57
+ }
58
+
59
+ /* ========================================================================== */
60
+
61
+ function objectDiff<T extends Record<string, any>>(
62
+ actual: T,
63
+ expected: T,
64
+ remarks: Remarks,
65
+ keys?: Set<string>,
66
+ ): ObjectDiff | ValueDiff {
67
+ // default keys: all keys from both actual and expected objects
68
+ if (! keys) keys = new Set([ ...Object.keys(actual), ...Object.keys(expected) ])
69
+
70
+ // no keys? no diff!
71
+ if (! keys.size) return { diff: false, value: actual }
72
+
73
+ // evaluate differences between objects
74
+ let diff = false
75
+ const props: Record<string, Diff> = {}
76
+ for (const key of keys) {
77
+ const act = actual[key]
78
+ const exp = expected[key]
79
+
80
+ let result = diffValues(act, exp, remarks)
81
+
82
+ // if there is a difference, we _might_ have a missing/extra property
83
+ if (result.diff) {
84
+ if ((act === undefined) && (key in expected)) {
85
+ result = { diff: true, missing: exp }
86
+ } else if ((exp === undefined) && (key in actual)) {
87
+ result = { diff: true, extra: act }
88
+ }
89
+ }
90
+
91
+ props[key] = result
92
+ diff ||= result.diff
93
+ }
94
+
95
+ // return our differences
96
+ return { diff, value: actual, props }
97
+ }
98
+
99
+ /* ========================================================================== */
100
+
101
+ function arrayDiff<T extends Record<number, any> & { length: number }>(
102
+ actual: T,
103
+ expected: T,
104
+ remarks: Remarks,
105
+ ): ObjectDiff {
106
+ // make sure that the length of both arrays is the same
107
+ if (actual.length !== expected.length) {
108
+ return errorDiff(actual, `to have length ${expected.length} (length=${actual.length})`)
109
+ }
110
+
111
+ // prepare a set with _all_ keys from both expected and actual object
112
+ const keys = new Set([ ...Object.keys(expected), ...Object.keys(actual) ])
113
+
114
+ // iterate through the array, checking equality for each item ad the given
115
+ // index, and _removing_ the index from the set of keys to further analyse
116
+ let valuesDiff = false
117
+ const values = new Array<Diff>(expected.length)
118
+ for (let i = 0; i < expected.length; i ++) {
119
+ const result = values[i] = diffValues(actual[i], expected[i], remarks)
120
+ valuesDiff = valuesDiff || result.diff
121
+ keys.delete(String(i))
122
+ }
123
+
124
+ // analyse the differences between leftover props (if any)
125
+ const result = objectDiff(actual, expected, remarks, keys)
126
+ const diff = result.diff || valuesDiff
127
+
128
+ // all done!
129
+ return { ...result, diff, values }
130
+ }
131
+
132
+ /* ========================================================================== */
133
+
134
+ function setDiff<T>(
135
+ actual: Set<T>,
136
+ expected: Set<T>,
137
+ remarks: Remarks,
138
+ ): ObjectDiff {
139
+ // highlight if sets have different sizes... don't expand the returned error
140
+ // as this might inject an extra "error: undefined" property in the final diff
141
+ const error = actual.size === expected.size ? {} :
142
+ errorDiff(actual, `to have size ${expected.size} (size=${actual.size})`)
143
+
144
+ // check differences between sets
145
+ const values: Diff[] = []
146
+ const missing = new Set<T>(expected)
147
+ const extra = new Set<T>(actual)
148
+
149
+ for (const act of extra) {
150
+ for (const exp of missing) {
151
+ const diff = diffValues(act, exp, remarks)
152
+ if (diff.diff) continue
153
+
154
+ values.push(diff)
155
+ extra.delete(act)
156
+ missing.delete(exp)
157
+ }
158
+ }
159
+
160
+ // compare sets as "objects" (for extra properties)
161
+ const result = objectDiff(actual, expected, remarks)
162
+ const diff = !! (missing.size || extra.size || result.diff)
163
+
164
+ // inject all extra and missing properties
165
+ extra.forEach((value) => values.push({ diff: true, extra: value }))
166
+ missing.forEach((value) => values.push({ diff: true, missing: value }))
167
+
168
+ // done...
169
+ return { ...error, ...result, diff, values }
170
+ }
171
+
172
+ /* ========================================================================== */
173
+
174
+ function mapDiff<K, V>(
175
+ actual: Map<K, V>,
176
+ expected: Map<K, V>,
177
+ remarks: Remarks,
178
+ ): ObjectDiff {
179
+ // check mappings
180
+ let diff = false
181
+ const mappings: [ any, Diff ][] = []
182
+ for (const key of new Set([ ...actual.keys(), ...expected.keys() ])) {
183
+ const act = actual.get(key)
184
+ const exp = expected.get(key)
185
+
186
+ if (! actual.has(key)) {
187
+ mappings.push([ key, { diff: true, missing: exp } ])
188
+ diff = true
189
+ } else if (! expected.has(key)) {
190
+ mappings.push([ key, { diff: true, extra: act } ])
191
+ diff = true
192
+ } else {
193
+ const result = diffValues(act, exp, remarks)
194
+ mappings.push([ key, result ])
195
+ diff = diff || result.diff
196
+ }
197
+ }
198
+
199
+ // check other properties
200
+ const result = objectDiff(actual, expected, remarks)
201
+ diff = diff || result.diff
202
+
203
+ // done...
204
+ return { ...result, diff, mappings }
205
+ }
206
+
207
+ /* ========================================================================== */
208
+
209
+ function binaryDiff<T extends Binary>(
210
+ actual: T,
211
+ expected: T,
212
+ actualData: Buffer,
213
+ expectedData: Buffer,
214
+ remarks: Remarks,
215
+ ): ObjectDiff | ValueDiff {
216
+ // make sure that the length of both arrays is the same
217
+ if (actualData.length !== expectedData.length) {
218
+ return errorDiff(actual, `to have length ${expectedData.length} (length=${actualData.length})`)
219
+ }
220
+
221
+ // remember keys
222
+ const keys = new Set([ ...Object.keys(expected), ...Object.keys(actual) ])
223
+
224
+ // check for equality
225
+ const length = expectedData.length
226
+ for (let i = 0; i < length; i ++) {
227
+ keys.delete(String(i))
228
+ if (actualData[i] === expectedData[i]) continue
229
+
230
+ let act = actualData.toString('hex', i, i + 6)
231
+ let exp = expectedData.toString('hex', i, i + 6)
232
+ if (act.length > 10) {
233
+ act = act.substring(0, 10) + '\u2026'
234
+ exp = exp.substring(0, 10) + '\u2026'
235
+ }
236
+ if (i > 0) {
237
+ act = '\u2026' + act
238
+ exp = '\u2026' + exp
239
+ }
240
+
241
+ return errorDiff(actual, `to equal at index ${i} (actual=${act}, expected=${exp})`)
242
+ }
243
+
244
+ // same contents, check extra properties
245
+ return objectDiff(actual, expected, remarks, keys)
246
+ }
247
+
248
+ /* ========================================================================== */
249
+
250
+ function primitiveDiff<T extends BoxedPrimitive>(
251
+ actual: T,
252
+ expected: T,
253
+ remarks: Remarks,
254
+ ): ObjectDiff | ExpectedDiff | ValueDiff {
255
+ if (actual.valueOf() !== expected.valueOf()) {
256
+ return {
257
+ diff: true,
258
+ value: actual,
259
+ expected: expected,
260
+ }
261
+ }
262
+
263
+ // remove string indexes from properties
264
+ const keys = new Set([ ...Object.keys(actual), ...Object.keys(expected) ])
265
+ if (actual instanceof String) {
266
+ const length = actual.valueOf().length
267
+ for (let i = 0; i < length; i ++) {
268
+ keys.delete(String(i))
269
+ }
270
+ }
271
+
272
+ // return either an object diff or no diff at all...
273
+ return keys.size ? objectDiff(actual, expected, remarks, keys) : {
274
+ diff: false,
275
+ value: actual,
276
+ }
277
+ }
278
+
279
+ /* ========================================================================== *
280
+ * MAIN "DIFF" *
281
+ * ========================================================================== */
282
+
283
+ function diffValues(actual: any, expected: any, remarks: Remarks): Diff {
284
+ // strict equality
285
+ if (expected === actual) return { diff: false, value: expected }
286
+
287
+ // oh, javascript!
288
+ if (expected === null) {
289
+ return {
290
+ diff: true,
291
+ value: actual,
292
+ expected: null,
293
+ }
294
+ }
295
+
296
+ // matchers!
297
+ if (isMatcher(expected)) {
298
+ try {
299
+ expected.expect(actual)
300
+ return { diff: false, value: actual }
301
+ } catch (error) {
302
+ if (error instanceof ExpectationError) {
303
+ // if the error highlights a difference, simply return that
304
+ // otherwise wrap the error into a new ErrorDiff and return it
305
+ const { message, diff } = error
306
+ return diff?.diff ? diff : { diff: true, value: actual, error: message }
307
+ } else {
308
+ throw error
309
+ }
310
+ }
311
+ }
312
+
313
+ // inspect types of what to compare
314
+ const actualType = typeof actual
315
+ const expectedType = typeof expected
316
+
317
+ // if type is different then highlight the difference
318
+ if (actualType !== expectedType) {
319
+ return {
320
+ diff: true,
321
+ value: actual,
322
+ expected: expected,
323
+ }
324
+ }
325
+
326
+ // primitives
327
+ switch (actualType) {
328
+ // numbers are one-of-a-kind as NaN !== NaN
329
+ case 'number':
330
+ if (isNaN(expected as number) && isNaN(actual as number)) {
331
+ return { diff: false, value: NaN }
332
+ }
333
+ // primitives always must be strict ===
334
+ case 'bigint':
335
+ case 'boolean':
336
+ case 'function':
337
+ case 'string':
338
+ case 'symbol':
339
+ case 'undefined':
340
+ return {
341
+ diff: true,
342
+ value: actual,
343
+ expected: expected,
344
+ }
345
+ // everything else is an object and must be checked
346
+ }
347
+
348
+ // check for cyclical dependencies, see node's commit here:
349
+ // https://github.com/nodejs/node/commit/d3aafd02efd3a403d646a3044adcf14e63a88d32
350
+ const actualIndex = remarks.actualMemos.indexOf(actual)
351
+ if (actualIndex !== -1) {
352
+ if (actualIndex === remarks.expectedMemos.indexOf(expected)) {
353
+ return { diff: false, value: actual }
354
+ }
355
+ }
356
+
357
+ remarks.actualMemos.push(actual)
358
+ remarks.expectedMemos.push(expected)
359
+
360
+ // check that actual is _assignable_ from expected
361
+ const prototype = Object.getPrototypeOf(expected)
362
+ if (prototype && prototype.constructor) {
363
+ if (! (actual instanceof prototype.constructor)) {
364
+ return {
365
+ ...errorDiff(actual, `to be instance of ${stringifyConstructor(prototype.constructor)}`),
366
+ expected,
367
+ }
368
+ }
369
+ }
370
+
371
+ /* == ARRAYS ============================================================== */
372
+
373
+ const checkInstance = <T>(
374
+ ctor: Constructor<T>,
375
+ callback: (actual: T, expected: T, remarks: Remarks) => Diff,
376
+ ): Diff | undefined =>
377
+ (expected instanceof ctor) ?
378
+ callback(actual as InstanceType<typeof ctor>, expected, remarks) :
379
+ (actual instanceof ctor) ?
380
+ { diff: true, value: actual, expected } :
381
+ undefined
382
+
383
+ return (
384
+ /* == ARRAYS ============================================================ */
385
+ checkInstance(Array, arrayDiff) ||
386
+
387
+ /* == SETS ============================================================== */
388
+ checkInstance(Set, (act, exp) => setDiff(act, exp, remarks)) ||
389
+
390
+ /* == MAPS ============================================================== */
391
+ checkInstance(Map, (act, exp) => mapDiff(act, exp, remarks)) ||
392
+
393
+ /* == BOXED PRIMITIVES ================================================== */
394
+ checkInstance(Boolean, primitiveDiff) ||
395
+ checkInstance(String, primitiveDiff) ||
396
+ checkInstance(Number, primitiveDiff) ||
397
+
398
+ /* == PROMISES (always error, must be ===) ============================== */
399
+ checkInstance(Promise, (act, exp) => errorDiff(act, `to strictly equal ${stringifyValue(exp)}`)) ||
400
+
401
+ /* == DATES ============================================================= */
402
+ checkInstance(Date, (act, exp) =>
403
+ (act.getTime() !== exp.getTime()) ? {
404
+ diff: true,
405
+ value: act,
406
+ expected: exp,
407
+ } : objectDiff(act, exp, remarks)) ||
408
+
409
+ /* == REGULAR EXPRESSIONS =============================================== */
410
+ checkInstance(RegExp, (act, exp) =>
411
+ ((act.source !== exp.source) || (act.flags !== exp.flags)) ? {
412
+ diff: true,
413
+ value: act,
414
+ expected: exp,
415
+ } : objectDiff(act, exp, remarks)) ||
416
+
417
+ /* == BINARY ARRAYS ===================================================== */
418
+ checkInstance(Buffer, (act, exp) => binaryDiff(act, exp, act, exp, remarks)) ||
419
+ checkInstance(Uint8Array, (act, exp) => binaryDiff(act, exp, Buffer.from(act), Buffer.from(exp), remarks)) ||
420
+ checkInstance(ArrayBuffer, (act, exp) => binaryDiff(act, exp, Buffer.from(act), Buffer.from(exp), remarks)) ||
421
+ checkInstance(SharedArrayBuffer, (act, exp) => binaryDiff(act, exp, Buffer.from(act), Buffer.from(exp), remarks)) ||
422
+
423
+ /* == OTHER TYPED ARRAYS ================================================ */
424
+ checkInstance(BigInt64Array, arrayDiff) ||
425
+ checkInstance(BigUint64Array, arrayDiff) ||
426
+ checkInstance(Float32Array, arrayDiff) ||
427
+ checkInstance(Float64Array, arrayDiff) ||
428
+ checkInstance(Int16Array, arrayDiff) ||
429
+ checkInstance(Int32Array, arrayDiff) ||
430
+ checkInstance(Int8Array, arrayDiff) ||
431
+ checkInstance(Uint16Array, arrayDiff) ||
432
+ checkInstance(Uint32Array, arrayDiff) ||
433
+ checkInstance(Uint8ClampedArray, arrayDiff) ||
434
+
435
+ /* == DONE ============================================================== */
436
+ objectDiff(actual as any, expected, remarks))
437
+ }
438
+
439
+ /* ========================================================================== *
440
+ * EXPORTS *
441
+ * ========================================================================== */
442
+
443
+ export function diff(actual: any, expected: any): Diff {
444
+ return diffValues(actual, expected, { actualMemos: [], expectedMemos: [] })
445
+ }