@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.
- package/README.md +7 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +96 -0
- package/dist/cli.mjs.map +6 -0
- package/dist/execution/executable.cjs +299 -0
- package/dist/execution/executable.cjs.map +6 -0
- package/dist/execution/executable.d.ts +87 -0
- package/dist/execution/executable.mjs +260 -0
- package/dist/execution/executable.mjs.map +6 -0
- package/dist/execution/executor.cjs +125 -0
- package/dist/execution/executor.cjs.map +6 -0
- package/dist/execution/executor.d.ts +35 -0
- package/dist/execution/executor.mjs +90 -0
- package/dist/execution/executor.mjs.map +6 -0
- package/dist/execution/setup.cjs +127 -0
- package/dist/execution/setup.cjs.map +6 -0
- package/dist/execution/setup.d.ts +31 -0
- package/dist/execution/setup.mjs +87 -0
- package/dist/execution/setup.mjs.map +6 -0
- package/dist/expectation/basic.cjs +216 -0
- package/dist/expectation/basic.cjs.map +6 -0
- package/dist/expectation/basic.d.ts +47 -0
- package/dist/expectation/basic.mjs +177 -0
- package/dist/expectation/basic.mjs.map +6 -0
- package/dist/expectation/diff.cjs +253 -0
- package/dist/expectation/diff.cjs.map +6 -0
- package/dist/expectation/diff.d.ts +27 -0
- package/dist/expectation/diff.mjs +228 -0
- package/dist/expectation/diff.mjs.map +6 -0
- package/dist/expectation/expect.cjs +211 -0
- package/dist/expectation/expect.cjs.map +6 -0
- package/dist/expectation/expect.d.ts +140 -0
- package/dist/expectation/expect.mjs +219 -0
- package/dist/expectation/expect.mjs.map +6 -0
- package/dist/expectation/include.cjs +187 -0
- package/dist/expectation/include.cjs.map +6 -0
- package/dist/expectation/include.d.ts +10 -0
- package/dist/expectation/include.mjs +158 -0
- package/dist/expectation/include.mjs.map +6 -0
- package/dist/expectation/print.cjs +281 -0
- package/dist/expectation/print.cjs.map +6 -0
- package/dist/expectation/print.d.ts +4 -0
- package/dist/expectation/print.mjs +256 -0
- package/dist/expectation/print.mjs.map +6 -0
- package/dist/expectation/throwing.cjs +58 -0
- package/dist/expectation/throwing.cjs.map +6 -0
- package/dist/expectation/throwing.d.ts +8 -0
- package/dist/expectation/throwing.mjs +32 -0
- package/dist/expectation/throwing.mjs.map +6 -0
- package/dist/expectation/types.cjs +212 -0
- package/dist/expectation/types.cjs.map +6 -0
- package/dist/expectation/types.d.ts +57 -0
- package/dist/expectation/types.mjs +178 -0
- package/dist/expectation/types.mjs.map +6 -0
- package/dist/expectation/void.cjs +111 -0
- package/dist/expectation/void.cjs.map +6 -0
- package/dist/expectation/void.d.ts +39 -0
- package/dist/expectation/void.mjs +77 -0
- package/dist/expectation/void.mjs.map +6 -0
- package/dist/globals.cjs +2 -0
- package/dist/globals.cjs.map +6 -0
- package/dist/globals.d.ts +23 -0
- package/dist/globals.mjs +1 -0
- package/dist/globals.mjs.map +6 -0
- package/dist/index.cjs +66 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.mjs +41 -0
- package/dist/index.mjs.map +6 -0
- package/dist/test.cjs +229 -0
- package/dist/test.cjs.map +6 -0
- package/dist/test.d.ts +9 -0
- package/dist/test.mjs +194 -0
- package/dist/test.mjs.map +6 -0
- package/package.json +57 -0
- package/src/cli.mts +122 -0
- package/src/execution/executable.ts +364 -0
- package/src/execution/executor.ts +146 -0
- package/src/execution/setup.ts +108 -0
- package/src/expectation/basic.ts +209 -0
- package/src/expectation/diff.ts +445 -0
- package/src/expectation/expect.ts +401 -0
- package/src/expectation/include.ts +184 -0
- package/src/expectation/print.ts +386 -0
- package/src/expectation/throwing.ts +45 -0
- package/src/expectation/types.ts +263 -0
- package/src/expectation/void.ts +80 -0
- package/src/globals.ts +30 -0
- package/src/index.ts +54 -0
- 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
|
+
}
|