@nexustools/nwjstestrunner 0.0.0-stage → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +21 -0
- package/README.md +142 -2
- package/bin/nwjstestrunner.js +13 -0
- package/dist/cli/nwjs-cli-launcher.d.ts +10 -0
- package/dist/cli/nwjs-cli-launcher.d.ts.map +1 -0
- package/dist/cli/nwjs-cli-launcher.js +281 -0
- package/dist/cli/nwjs-cli-launcher.js.map +1 -0
- package/dist/config/harness-config-loader.d.ts +10 -0
- package/dist/config/harness-config-loader.d.ts.map +1 -0
- package/dist/config/harness-config-loader.js +242 -0
- package/dist/config/harness-config-loader.js.map +1 -0
- package/dist/core/test-runner.d.ts +61 -0
- package/dist/core/test-runner.d.ts.map +1 -0
- package/dist/core/test-runner.js +450 -0
- package/dist/core/test-runner.js.map +1 -0
- package/dist/esm/index.d.mts +2 -0
- package/dist/esm/index.mjs +26 -0
- package/dist/harness/graphical-overlay-view.d.ts +18 -0
- package/dist/harness/graphical-overlay-view.d.ts.map +1 -0
- package/dist/harness/graphical-overlay-view.js +213 -0
- package/dist/harness/graphical-overlay-view.js.map +1 -0
- package/dist/harness/nwjs-graphical-test-harness.d.ts +40 -0
- package/dist/harness/nwjs-graphical-test-harness.d.ts.map +1 -0
- package/dist/harness/nwjs-graphical-test-harness.js +398 -0
- package/dist/harness/nwjs-graphical-test-harness.js.map +1 -0
- package/dist/harness/test-http-server.d.ts +12 -0
- package/dist/harness/test-http-server.d.ts.map +1 -0
- package/dist/harness/test-http-server.js +183 -0
- package/dist/harness/test-http-server.js.map +1 -0
- package/dist/harness/test-log-writer.d.ts +29 -0
- package/dist/harness/test-log-writer.d.ts.map +1 -0
- package/dist/harness/test-log-writer.js +173 -0
- package/dist/harness/test-log-writer.js.map +1 -0
- package/dist/harness/webview-test-controller.d.ts +18 -0
- package/dist/harness/webview-test-controller.d.ts.map +1 -0
- package/dist/harness/webview-test-controller.js +163 -0
- package/dist/harness/webview-test-controller.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/matchers/deep-equality-helper.d.ts +10 -0
- package/dist/matchers/deep-equality-helper.d.ts.map +1 -0
- package/dist/matchers/deep-equality-helper.js +188 -0
- package/dist/matchers/deep-equality-helper.js.map +1 -0
- package/dist/matchers/expect-matcher.d.ts +7 -0
- package/dist/matchers/expect-matcher.d.ts.map +1 -0
- package/dist/matchers/expect-matcher.js +261 -0
- package/dist/matchers/expect-matcher.js.map +1 -0
- package/dist/mock/jest-mock-factory.d.ts +20 -0
- package/dist/mock/jest-mock-factory.d.ts.map +1 -0
- package/dist/mock/jest-mock-factory.js +141 -0
- package/dist/mock/jest-mock-factory.js.map +1 -0
- package/dist/tests/NwjsTestRunnerSuites.d.ts +3 -0
- package/dist/tests/NwjsTestRunnerSuites.d.ts.map +1 -0
- package/dist/tests/NwjsTestRunnerSuites.js +266 -0
- package/dist/tests/NwjsTestRunnerSuites.js.map +1 -0
- package/dist/types/test-runner-types.d.ts +321 -0
- package/dist/types/test-runner-types.d.ts.map +1 -0
- package/dist/types/test-runner-types.js +3 -0
- package/dist/types/test-runner-types.js.map +1 -0
- package/package.json +56 -4
- package/runner/demo-client.html +70 -0
- package/runner/index.html +223 -0
- package/runner/package.json +27 -0
- package/src/cli/nwjs-cli-launcher.ts +243 -0
- package/src/config/harness-config-loader.ts +214 -0
- package/src/core/test-runner.ts +553 -0
- package/src/harness/graphical-overlay-view.ts +219 -0
- package/src/harness/nwjs-graphical-test-harness.ts +468 -0
- package/src/harness/test-http-server.ts +162 -0
- package/src/harness/test-log-writer.ts +154 -0
- package/src/harness/webview-test-controller.ts +192 -0
- package/src/index.ts +28 -0
- package/src/matchers/deep-equality-helper.ts +193 -0
- package/src/matchers/expect-matcher.ts +361 -0
- package/src/mock/jest-mock-factory.ts +170 -0
- package/src/tests/NwjsTestRunnerSuites.ts +267 -0
- package/src/types/test-runner-types.ts +335 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export class DeepEqualityHelper {
|
|
2
|
+
public static deepEquals(a: any, b: any, strict = false, seen = new Map<any, any>()): boolean {
|
|
3
|
+
if (Object.is(a, b)) {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (a === null || b === null || a === undefined || b === undefined) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (typeof a !== typeof b) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof a !== 'object') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (seen.has(a) && seen.get(a) === b) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
seen.set(a, b);
|
|
23
|
+
|
|
24
|
+
if (strict && a.constructor !== b.constructor) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (a instanceof Date && b instanceof Date) {
|
|
29
|
+
return Object.is(a.getTime(), b.getTime());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (a instanceof RegExp && b instanceof RegExp) {
|
|
33
|
+
return a.source === b.source && a.flags === b.flags;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (a instanceof Error && b instanceof Error) {
|
|
37
|
+
return a.name === b.name && a.message === b.message;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) {
|
|
41
|
+
return a.equals(b);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
|
|
45
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
46
|
+
const ua = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
|
|
47
|
+
const ub = new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
|
|
48
|
+
for (let i = 0; i < ua.length; i++) {
|
|
49
|
+
if (ua[i] !== ub[i]) return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (a instanceof Map && b instanceof Map) {
|
|
55
|
+
if (a.size !== b.size) return false;
|
|
56
|
+
for (const [k, v] of a.entries()) {
|
|
57
|
+
if (!b.has(k) || !DeepEqualityHelper.deepEquals(v, b.get(k), strict, seen)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (a instanceof Set && b instanceof Set) {
|
|
65
|
+
if (a.size !== b.size) return false;
|
|
66
|
+
const bValues = Array.from(b.values());
|
|
67
|
+
for (const aVal of a.values()) {
|
|
68
|
+
const matchIdx = bValues.findIndex((bVal) => DeepEqualityHelper.deepEquals(aVal, bVal, strict, seen));
|
|
69
|
+
if (matchIdx === -1) return false;
|
|
70
|
+
bValues.splice(matchIdx, 1);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
76
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
77
|
+
if (a.length !== b.length) return false;
|
|
78
|
+
for (let i = 0; i < a.length; i++) {
|
|
79
|
+
if (strict && (i in a) !== (i in b)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (!DeepEqualityHelper.deepEquals(a[i], b[i], strict, seen)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const keysA = strict
|
|
90
|
+
? Reflect.ownKeys(a)
|
|
91
|
+
: Object.keys(a).filter((k) => a[k] !== undefined);
|
|
92
|
+
const keysB = strict
|
|
93
|
+
? Reflect.ownKeys(b)
|
|
94
|
+
: Object.keys(b).filter((k) => b[k] !== undefined);
|
|
95
|
+
|
|
96
|
+
if (keysA.length !== keysB.length) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const key of keysA) {
|
|
101
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) {
|
|
102
|
+
if (strict || b[key as any] !== undefined) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!DeepEqualityHelper.deepEquals(a[key as any], b[key as any], strict, seen)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public static matchesObject(actual: any, expected: any): boolean {
|
|
115
|
+
if (Object.is(actual, expected)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (actual === null || actual === undefined || typeof actual !== 'object') {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (expected === null || expected === undefined || typeof expected !== 'object') {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (Array.isArray(expected)) {
|
|
126
|
+
if (!Array.isArray(actual) || actual.length < expected.length) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
for (let i = 0; i < expected.length; i++) {
|
|
130
|
+
if (!DeepEqualityHelper.matchesObject(actual[i], expected[i])) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const key of Object.keys(expected)) {
|
|
138
|
+
if (!(key in actual)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const expVal = expected[key];
|
|
142
|
+
const actVal = actual[key];
|
|
143
|
+
if (expVal !== null && typeof expVal === 'object' && !(expVal instanceof RegExp) && !(expVal instanceof Date)) {
|
|
144
|
+
if (!DeepEqualityHelper.matchesObject(actVal, expVal)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
} else if (!DeepEqualityHelper.deepEquals(actVal, expVal)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public static resolvePropertyPath(
|
|
156
|
+
target: any,
|
|
157
|
+
keyPath: string | string[]
|
|
158
|
+
): { exists: boolean; value: any } {
|
|
159
|
+
if (target === null || target === undefined) {
|
|
160
|
+
return { exists: false, value: undefined };
|
|
161
|
+
}
|
|
162
|
+
const segments = Array.isArray(keyPath) ? keyPath : keyPath.split('.');
|
|
163
|
+
let current = target;
|
|
164
|
+
for (const seg of segments) {
|
|
165
|
+
if (current === null || current === undefined) {
|
|
166
|
+
return { exists: false, value: undefined };
|
|
167
|
+
}
|
|
168
|
+
if (typeof current === 'object' || typeof current === 'function') {
|
|
169
|
+
if (!(seg in Object(current))) {
|
|
170
|
+
return { exists: false, value: undefined };
|
|
171
|
+
}
|
|
172
|
+
current = current[seg];
|
|
173
|
+
} else {
|
|
174
|
+
return { exists: false, value: undefined };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { exists: true, value: current };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public static formatValue(value: any): string {
|
|
181
|
+
if (value === undefined) return 'undefined';
|
|
182
|
+
if (value === null) return 'null';
|
|
183
|
+
if (typeof value === 'number' && Number.isNaN(value)) return 'NaN';
|
|
184
|
+
if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`;
|
|
185
|
+
if (value instanceof RegExp) return value.toString();
|
|
186
|
+
if (value instanceof Error) return `[${value.name}: ${value.message}]`;
|
|
187
|
+
try {
|
|
188
|
+
return JSON.stringify(value);
|
|
189
|
+
} catch (_e) {
|
|
190
|
+
return String(value);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { AsyncExpectMatcher, ExpectMatcher } from '../types/test-runner-types';
|
|
2
|
+
import { DeepEqualityHelper } from './deep-equality-helper';
|
|
3
|
+
|
|
4
|
+
export class ExpectMatcherBuilder {
|
|
5
|
+
public static createMatcher(actual: any, isNot = false): ExpectMatcher {
|
|
6
|
+
const check = (condition: boolean, msg: string): void => {
|
|
7
|
+
const pass = isNot ? !condition : condition;
|
|
8
|
+
if (!pass) {
|
|
9
|
+
throw new Error(msg);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const notPrefix = isNot ? 'not.' : '';
|
|
14
|
+
const fmt = DeepEqualityHelper.formatValue;
|
|
15
|
+
|
|
16
|
+
const verifyErrorMatch = (
|
|
17
|
+
caughtError: any,
|
|
18
|
+
expectedError?: string | RegExp | Error | (new (...args: any[]) => Error)
|
|
19
|
+
): boolean => {
|
|
20
|
+
if (expectedError === undefined) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const message =
|
|
24
|
+
caughtError instanceof Error
|
|
25
|
+
? caughtError.message
|
|
26
|
+
: typeof caughtError === 'string'
|
|
27
|
+
? caughtError
|
|
28
|
+
: String(caughtError);
|
|
29
|
+
|
|
30
|
+
if (typeof expectedError === 'string') {
|
|
31
|
+
return message.includes(expectedError);
|
|
32
|
+
}
|
|
33
|
+
if (expectedError instanceof RegExp) {
|
|
34
|
+
return expectedError.test(message);
|
|
35
|
+
}
|
|
36
|
+
if (expectedError instanceof Error) {
|
|
37
|
+
return message.includes(expectedError.message);
|
|
38
|
+
}
|
|
39
|
+
if (typeof expectedError === 'function') {
|
|
40
|
+
return caughtError instanceof expectedError;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const matchers: ExpectMatcher = {
|
|
46
|
+
toBe(expected: any): void {
|
|
47
|
+
check(
|
|
48
|
+
Object.is(actual, expected) || actual === expected,
|
|
49
|
+
`expect(${fmt(actual)}).${notPrefix}toBe(${fmt(expected)}) failed`
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
toEqual(expected: any): void {
|
|
54
|
+
const match = DeepEqualityHelper.deepEquals(actual, expected, false);
|
|
55
|
+
check(match, `expect(${fmt(actual)}).${notPrefix}toEqual(${fmt(expected)}) failed`);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
toStrictEqual(expected: any): void {
|
|
59
|
+
const match = DeepEqualityHelper.deepEquals(actual, expected, true);
|
|
60
|
+
check(match, `expect(${fmt(actual)}).${notPrefix}toStrictEqual(${fmt(expected)}) failed`);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
toBeTruthy(): void {
|
|
64
|
+
check(Boolean(actual) === true, `expect(${fmt(actual)}).${notPrefix}toBeTruthy() failed`);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
toBeFalsy(): void {
|
|
68
|
+
check(Boolean(actual) === false, `expect(${fmt(actual)}).${notPrefix}toBeFalsy() failed`);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
toBeNull(): void {
|
|
72
|
+
check(actual === null, `expect(${fmt(actual)}).${notPrefix}toBeNull() failed`);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
toBeUndefined(): void {
|
|
76
|
+
check(actual === undefined, `expect(${fmt(actual)}).${notPrefix}toBeUndefined() failed`);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
toBeDefined(): void {
|
|
80
|
+
check(
|
|
81
|
+
actual !== undefined && actual !== null,
|
|
82
|
+
`expect(${fmt(actual)}).${notPrefix}toBeDefined() failed`
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
toBeNaN(): void {
|
|
87
|
+
check(Number.isNaN(actual), `expect(${fmt(actual)}).${notPrefix}toBeNaN() failed`);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
toBeInstanceOf(expectedConstructor: any): void {
|
|
91
|
+
const isInst =
|
|
92
|
+
typeof expectedConstructor === 'function' && actual instanceof expectedConstructor;
|
|
93
|
+
const ctorName = expectedConstructor?.name || fmt(expectedConstructor);
|
|
94
|
+
check(
|
|
95
|
+
isInst,
|
|
96
|
+
`expect(${fmt(actual)}).${notPrefix}toBeInstanceOf(${ctorName}) failed`
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
toBeGreaterThan(expected: number | bigint): void {
|
|
101
|
+
check(
|
|
102
|
+
actual > expected,
|
|
103
|
+
`expect(${fmt(actual)}).${notPrefix}toBeGreaterThan(${fmt(expected)}) failed`
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
toBeGreaterThanOrEqual(expected: number | bigint): void {
|
|
108
|
+
check(
|
|
109
|
+
actual >= expected,
|
|
110
|
+
`expect(${fmt(actual)}).${notPrefix}toBeGreaterThanOrEqual(${fmt(expected)}) failed`
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
toBeLessThan(expected: number | bigint): void {
|
|
115
|
+
check(
|
|
116
|
+
actual < expected,
|
|
117
|
+
`expect(${fmt(actual)}).${notPrefix}toBeLessThan(${fmt(expected)}) failed`
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
toBeLessThanOrEqual(expected: number | bigint): void {
|
|
122
|
+
check(
|
|
123
|
+
actual <= expected,
|
|
124
|
+
`expect(${fmt(actual)}).${notPrefix}toBeLessThanOrEqual(${fmt(expected)}) failed`
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
toBeCloseTo(expected: number, numDigits = 2): void {
|
|
129
|
+
const precision = Math.pow(10, -numDigits) / 2;
|
|
130
|
+
const diff = Math.abs(Number(actual) - expected);
|
|
131
|
+
check(
|
|
132
|
+
!Number.isNaN(diff) && diff < precision,
|
|
133
|
+
`expect(${fmt(actual)}).${notPrefix}toBeCloseTo(${fmt(expected)}, ${numDigits}) failed`
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
toContain(item: any): void {
|
|
138
|
+
let contains = false;
|
|
139
|
+
if (typeof actual === 'string') {
|
|
140
|
+
contains = actual.includes(String(item));
|
|
141
|
+
} else if (Array.isArray(actual)) {
|
|
142
|
+
contains = actual.includes(item);
|
|
143
|
+
} else if (actual instanceof Set) {
|
|
144
|
+
contains = actual.has(item);
|
|
145
|
+
} else if (actual && typeof actual[Symbol.iterator] === 'function') {
|
|
146
|
+
for (const entry of actual) {
|
|
147
|
+
if (entry === item) {
|
|
148
|
+
contains = true;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
check(contains, `expect(${fmt(actual)}).${notPrefix}toContain(${fmt(item)}) failed`);
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
toContainEqual(item: any): void {
|
|
157
|
+
let contains = false;
|
|
158
|
+
if (Array.isArray(actual) || (actual && typeof actual[Symbol.iterator] === 'function')) {
|
|
159
|
+
for (const entry of actual) {
|
|
160
|
+
if (DeepEqualityHelper.deepEquals(entry, item)) {
|
|
161
|
+
contains = true;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
check(
|
|
167
|
+
contains,
|
|
168
|
+
`expect(${fmt(actual)}).${notPrefix}toContainEqual(${fmt(item)}) failed`
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
toHaveLength(expectedLength: number): void {
|
|
173
|
+
const actualLen = actual !== null && actual !== undefined ? actual.length : undefined;
|
|
174
|
+
check(
|
|
175
|
+
actualLen === expectedLength,
|
|
176
|
+
`expect(${fmt(actual)}).${notPrefix}toHaveLength(${expectedLength}) failed (received length: ${fmt(actualLen)})`
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
toHaveProperty(keyPath: string | string[], expectedValue?: any): void {
|
|
181
|
+
const resolved = DeepEqualityHelper.resolvePropertyPath(actual, keyPath);
|
|
182
|
+
const hasValueCheck = arguments.length >= 2;
|
|
183
|
+
const passCondition =
|
|
184
|
+
resolved.exists &&
|
|
185
|
+
(!hasValueCheck || DeepEqualityHelper.deepEquals(resolved.value, expectedValue));
|
|
186
|
+
check(
|
|
187
|
+
passCondition,
|
|
188
|
+
`expect(${fmt(actual)}).${notPrefix}toHaveProperty(${fmt(keyPath)}${hasValueCheck ? `, ${fmt(expectedValue)}` : ''}) failed`
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
toMatch(expected: string | RegExp): void {
|
|
193
|
+
const str = typeof actual === 'string' ? actual : String(actual ?? '');
|
|
194
|
+
const matched =
|
|
195
|
+
expected instanceof RegExp ? expected.test(str) : str.includes(String(expected));
|
|
196
|
+
check(matched, `expect(${fmt(actual)}).${notPrefix}toMatch(${fmt(expected)}) failed`);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
toMatchObject(expected: Record<string, any> | any[]): void {
|
|
200
|
+
const matched = DeepEqualityHelper.matchesObject(actual, expected);
|
|
201
|
+
check(
|
|
202
|
+
matched,
|
|
203
|
+
`expect(${fmt(actual)}).${notPrefix}toMatchObject(${fmt(expected)}) failed`
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
toThrow(expectedError?: string | RegExp | Error | (new (...args: any[]) => Error)): void {
|
|
208
|
+
let threw = false;
|
|
209
|
+
let caught: any;
|
|
210
|
+
if (typeof actual === 'function') {
|
|
211
|
+
try {
|
|
212
|
+
actual();
|
|
213
|
+
} catch (err) {
|
|
214
|
+
threw = true;
|
|
215
|
+
caught = err;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const matched = threw && verifyErrorMatch(caught, expectedError);
|
|
219
|
+
check(
|
|
220
|
+
matched,
|
|
221
|
+
`expect(fn).${notPrefix}toThrow(${expectedError !== undefined ? fmt(expectedError) : ''}) failed: ${
|
|
222
|
+
!threw
|
|
223
|
+
? 'function did not throw'
|
|
224
|
+
: `threw ${fmt(caught)} which did not match ${fmt(expectedError)}`
|
|
225
|
+
}`
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
toHaveBeenCalled(): void {
|
|
230
|
+
if (!actual || !actual._isMockFunction || !actual.mock) {
|
|
231
|
+
throw new Error('expect(fn).toHaveBeenCalled() requires a mock/spy function');
|
|
232
|
+
}
|
|
233
|
+
check(
|
|
234
|
+
actual.mock.calls.length > 0,
|
|
235
|
+
`expect(mockFn).${notPrefix}toHaveBeenCalled() failed (called ${actual.mock.calls.length} times)`
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
toHaveBeenCalledTimes(expectedCount: number): void {
|
|
240
|
+
if (!actual || !actual._isMockFunction || !actual.mock) {
|
|
241
|
+
throw new Error('expect(fn).toHaveBeenCalledTimes() requires a mock/spy function');
|
|
242
|
+
}
|
|
243
|
+
check(
|
|
244
|
+
actual.mock.calls.length === expectedCount,
|
|
245
|
+
`expect(mockFn).${notPrefix}toHaveBeenCalledTimes(${expectedCount}) failed (called ${actual.mock.calls.length} times)`
|
|
246
|
+
);
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
toHaveBeenCalledWith(...expectedArgs: any[]): void {
|
|
250
|
+
if (!actual || !actual._isMockFunction || !actual.mock) {
|
|
251
|
+
throw new Error('expect(fn).toHaveBeenCalledWith() requires a mock/spy function');
|
|
252
|
+
}
|
|
253
|
+
const matched = actual.mock.calls.some((callArgs: any[]) =>
|
|
254
|
+
DeepEqualityHelper.deepEquals(callArgs, expectedArgs)
|
|
255
|
+
);
|
|
256
|
+
check(
|
|
257
|
+
matched,
|
|
258
|
+
`expect(mockFn).${notPrefix}toHaveBeenCalledWith(${expectedArgs.map(fmt).join(', ')}) failed (calls: ${fmt(actual.mock.calls)})`
|
|
259
|
+
);
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
toHaveBeenLastCalledWith(...expectedArgs: any[]): void {
|
|
263
|
+
if (!actual || !actual._isMockFunction || !actual.mock) {
|
|
264
|
+
throw new Error('expect(fn).toHaveBeenLastCalledWith() requires a mock/spy function');
|
|
265
|
+
}
|
|
266
|
+
const lastCall = actual.mock.lastCall;
|
|
267
|
+
const matched =
|
|
268
|
+
actual.mock.calls.length > 0 && DeepEqualityHelper.deepEquals(lastCall, expectedArgs);
|
|
269
|
+
check(
|
|
270
|
+
matched,
|
|
271
|
+
`expect(mockFn).${notPrefix}toHaveBeenLastCalledWith(${expectedArgs.map(fmt).join(', ')}) failed (lastCall: ${fmt(lastCall)})`
|
|
272
|
+
);
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
get not(): ExpectMatcher {
|
|
276
|
+
return ExpectMatcherBuilder.createMatcher(actual, !isNot);
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
get resolves(): AsyncExpectMatcher {
|
|
280
|
+
return ExpectMatcherBuilder.createAsyncMatcher(actual, 'resolves', false);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
get rejects(): AsyncExpectMatcher {
|
|
284
|
+
return ExpectMatcherBuilder.createAsyncMatcher(actual, 'rejects', false);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return matchers;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public static createAsyncMatcher(
|
|
292
|
+
promiseOrFn: any,
|
|
293
|
+
mode: 'resolves' | 'rejects',
|
|
294
|
+
isNot = false
|
|
295
|
+
): AsyncExpectMatcher {
|
|
296
|
+
const awaitTarget = async (): Promise<any> => {
|
|
297
|
+
const targetPromise =
|
|
298
|
+
typeof promiseOrFn === 'function' ? Promise.resolve().then(() => promiseOrFn()) : Promise.resolve(promiseOrFn);
|
|
299
|
+
|
|
300
|
+
if (mode === 'resolves') {
|
|
301
|
+
try {
|
|
302
|
+
return await targetPromise;
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`expect(promise).resolves failed because promise rejected with: ${DeepEqualityHelper.formatValue(err)}`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
let didReject = false;
|
|
310
|
+
let rejectedVal: any;
|
|
311
|
+
try {
|
|
312
|
+
await targetPromise;
|
|
313
|
+
} catch (err) {
|
|
314
|
+
didReject = true;
|
|
315
|
+
rejectedVal = err;
|
|
316
|
+
}
|
|
317
|
+
if (!didReject) {
|
|
318
|
+
throw new Error('expect(promise).rejects failed because promise resolved instead of rejecting');
|
|
319
|
+
}
|
|
320
|
+
return rejectedVal;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const runSyncMatcher = async (
|
|
325
|
+
invoke: (syncMatcher: ExpectMatcher, resolvedValue: any) => void
|
|
326
|
+
): Promise<void> => {
|
|
327
|
+
const val = await awaitTarget();
|
|
328
|
+
const syncMatcher = ExpectMatcherBuilder.createMatcher(val, isNot);
|
|
329
|
+
invoke(syncMatcher, val);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
toBe: (expected: any) => runSyncMatcher((m) => m.toBe(expected)),
|
|
334
|
+
toEqual: (expected: any) => runSyncMatcher((m) => m.toEqual(expected)),
|
|
335
|
+
toStrictEqual: (expected: any) => runSyncMatcher((m) => m.toStrictEqual(expected)),
|
|
336
|
+
toBeTruthy: () => runSyncMatcher((m) => m.toBeTruthy()),
|
|
337
|
+
toBeFalsy: () => runSyncMatcher((m) => m.toBeFalsy()),
|
|
338
|
+
toBeNull: () => runSyncMatcher((m) => m.toBeNull()),
|
|
339
|
+
toBeUndefined: () => runSyncMatcher((m) => m.toBeUndefined()),
|
|
340
|
+
toBeDefined: () => runSyncMatcher((m) => m.toBeDefined()),
|
|
341
|
+
toContain: (item: any) => runSyncMatcher((m) => m.toContain(item)),
|
|
342
|
+
toMatch: (expected: string | RegExp) => runSyncMatcher((m) => m.toMatch(expected)),
|
|
343
|
+
toMatchObject: (expected: Record<string, any> | any[]) =>
|
|
344
|
+
runSyncMatcher((m) => m.toMatchObject(expected)),
|
|
345
|
+
toThrow: async (expectedError?: string | RegExp | Error | (new (...args: any[]) => Error)) => {
|
|
346
|
+
const val = await awaitTarget();
|
|
347
|
+
const syncMatcher = ExpectMatcherBuilder.createMatcher(() => {
|
|
348
|
+
throw val;
|
|
349
|
+
}, isNot);
|
|
350
|
+
syncMatcher.toThrow(expectedError);
|
|
351
|
+
},
|
|
352
|
+
get not(): AsyncExpectMatcher {
|
|
353
|
+
return ExpectMatcherBuilder.createAsyncMatcher(promiseOrFn, mode, !isNot);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function expect(actual: any): ExpectMatcher {
|
|
360
|
+
return ExpectMatcherBuilder.createMatcher(actual, false);
|
|
361
|
+
}
|