@hyperfixi/testing-framework 2.0.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/dist/assertions-BImuwZsB.d.mts +458 -0
- package/dist/assertions-BImuwZsB.d.ts +458 -0
- package/dist/assertions.d.mts +1 -0
- package/dist/assertions.d.ts +1 -0
- package/dist/assertions.js +625 -0
- package/dist/assertions.js.map +1 -0
- package/dist/assertions.mjs +621 -0
- package/dist/assertions.mjs.map +1 -0
- package/dist/index.d.mts +246 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +1465 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1435 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +121 -0
- package/src/assertions.test.ts +490 -0
- package/src/assertions.ts +779 -0
- package/src/index.test.ts +490 -0
- package/src/index.ts +601 -0
- package/src/jsdom.d.ts +7 -0
- package/src/multilingual/README.md +295 -0
- package/src/multilingual/bundle-builder.ts +345 -0
- package/src/multilingual/cli.ts +224 -0
- package/src/multilingual/index.ts +68 -0
- package/src/multilingual/orchestrator.ts +275 -0
- package/src/multilingual/pattern-loader.ts +176 -0
- package/src/multilingual/reporters/console-reporter.ts +294 -0
- package/src/multilingual/reporters/json-reporter.ts +99 -0
- package/src/multilingual/reporters/regression-reporter.ts +263 -0
- package/src/multilingual/tools/analyze-failures.ts +326 -0
- package/src/multilingual/types.ts +268 -0
- package/src/multilingual/validators/parse-diagnostics.ts +337 -0
- package/src/multilingual/validators/parse-validator.ts +193 -0
- package/src/multilingual/validators/size-validator.ts +187 -0
- package/src/runner.test.ts +813 -0
- package/src/runner.ts +639 -0
- package/src/types.ts +505 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1435 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
import { diffLines } from 'diff';
|
|
3
|
+
|
|
4
|
+
/* Testing Framework for HyperFixi Applications */
|
|
5
|
+
|
|
6
|
+
var AssertionError = class extends Error {
|
|
7
|
+
constructor(message, expected, actual, operator) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.expected = expected;
|
|
10
|
+
this.actual = actual;
|
|
11
|
+
this.operator = operator;
|
|
12
|
+
this.name = "AssertionError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function deepEqual(actual, expected) {
|
|
16
|
+
if (actual === expected) return true;
|
|
17
|
+
if (actual == null || expected == null) return actual === expected;
|
|
18
|
+
if (typeof actual !== typeof expected) return false;
|
|
19
|
+
if (actual instanceof Date && expected instanceof Date) {
|
|
20
|
+
return actual.getTime() === expected.getTime();
|
|
21
|
+
}
|
|
22
|
+
if (actual instanceof RegExp && expected instanceof RegExp) {
|
|
23
|
+
return actual.toString() === expected.toString();
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(actual) && Array.isArray(expected)) {
|
|
26
|
+
if (actual.length !== expected.length) return false;
|
|
27
|
+
for (let i = 0; i < actual.length; i++) {
|
|
28
|
+
if (!deepEqual(actual[i], expected[i])) return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (typeof actual === "object" && typeof expected === "object") {
|
|
33
|
+
const actualKeys = Object.keys(actual);
|
|
34
|
+
const expectedKeys = Object.keys(expected);
|
|
35
|
+
if (actualKeys.length !== expectedKeys.length) return false;
|
|
36
|
+
for (const key of actualKeys) {
|
|
37
|
+
if (!expectedKeys.includes(key)) return false;
|
|
38
|
+
if (!deepEqual(actual[key], expected[key])) return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
function generateDiff(actual, expected) {
|
|
45
|
+
const actualStr = typeof actual === "string" ? actual : JSON.stringify(actual, null, 2);
|
|
46
|
+
const expectedStr = typeof expected === "string" ? expected : JSON.stringify(expected, null, 2);
|
|
47
|
+
const diff = diffLines(expectedStr, actualStr);
|
|
48
|
+
return diff.map((part) => {
|
|
49
|
+
const prefix = part.added ? "+" : part.removed ? "-" : " ";
|
|
50
|
+
return part.value.split("\n").map((line) => `${prefix} ${line}`).join("\n");
|
|
51
|
+
}).join("\n");
|
|
52
|
+
}
|
|
53
|
+
function isInDocument(element) {
|
|
54
|
+
if (typeof document === "undefined") return false;
|
|
55
|
+
return document.contains(element);
|
|
56
|
+
}
|
|
57
|
+
function isVisible(element) {
|
|
58
|
+
if (!isInDocument(element)) return false;
|
|
59
|
+
const style = window.getComputedStyle(element);
|
|
60
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
61
|
+
}
|
|
62
|
+
var ExpectMatcherImpl = class _ExpectMatcherImpl {
|
|
63
|
+
constructor(actual, isNot = false) {
|
|
64
|
+
this.actual = actual;
|
|
65
|
+
this.isNot = isNot;
|
|
66
|
+
}
|
|
67
|
+
get not() {
|
|
68
|
+
return new _ExpectMatcherImpl(this.actual, !this.isNot);
|
|
69
|
+
}
|
|
70
|
+
get resolves() {
|
|
71
|
+
if (!this.isPromise(this.actual)) {
|
|
72
|
+
throw new AssertionError("Expected value to be a Promise");
|
|
73
|
+
}
|
|
74
|
+
return new _ExpectMatcherImpl(
|
|
75
|
+
this.actual.then((value) => value),
|
|
76
|
+
this.isNot
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
get rejects() {
|
|
80
|
+
if (!this.isPromise(this.actual)) {
|
|
81
|
+
throw new AssertionError("Expected value to be a Promise");
|
|
82
|
+
}
|
|
83
|
+
return new _ExpectMatcherImpl(
|
|
84
|
+
this.actual.then(
|
|
85
|
+
(value) => {
|
|
86
|
+
throw new Error("Promise did not reject");
|
|
87
|
+
},
|
|
88
|
+
(error) => error
|
|
89
|
+
),
|
|
90
|
+
this.isNot
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
toBe(expected) {
|
|
94
|
+
const passed = this.actual === expected;
|
|
95
|
+
this.assert(
|
|
96
|
+
passed,
|
|
97
|
+
"toBe",
|
|
98
|
+
expected,
|
|
99
|
+
`Expected ${this.format(this.actual)} to be ${this.format(expected)}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
toEqual(expected) {
|
|
103
|
+
const passed = deepEqual(this.actual, expected);
|
|
104
|
+
this.assert(
|
|
105
|
+
passed,
|
|
106
|
+
"toEqual",
|
|
107
|
+
expected,
|
|
108
|
+
`Expected ${this.format(this.actual)} to equal ${this.format(expected)}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
toStrictEqual(expected) {
|
|
112
|
+
const passed = this.actual === expected && deepEqual(this.actual, expected);
|
|
113
|
+
this.assert(
|
|
114
|
+
passed,
|
|
115
|
+
"toStrictEqual",
|
|
116
|
+
expected,
|
|
117
|
+
`Expected ${this.format(this.actual)} to strictly equal ${this.format(expected)}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
toBeTruthy() {
|
|
121
|
+
const passed = Boolean(this.actual);
|
|
122
|
+
this.assert(passed, "toBeTruthy", true, `Expected ${this.format(this.actual)} to be truthy`);
|
|
123
|
+
}
|
|
124
|
+
toBeFalsy() {
|
|
125
|
+
const passed = !Boolean(this.actual);
|
|
126
|
+
this.assert(passed, "toBeFalsy", false, `Expected ${this.format(this.actual)} to be falsy`);
|
|
127
|
+
}
|
|
128
|
+
toBeNull() {
|
|
129
|
+
const passed = this.actual === null;
|
|
130
|
+
this.assert(passed, "toBeNull", null, `Expected ${this.format(this.actual)} to be null`);
|
|
131
|
+
}
|
|
132
|
+
toBeUndefined() {
|
|
133
|
+
const passed = this.actual === void 0;
|
|
134
|
+
this.assert(
|
|
135
|
+
passed,
|
|
136
|
+
"toBeUndefined",
|
|
137
|
+
void 0,
|
|
138
|
+
`Expected ${this.format(this.actual)} to be undefined`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
toBeDefined() {
|
|
142
|
+
const passed = this.actual !== void 0;
|
|
143
|
+
this.assert(
|
|
144
|
+
passed,
|
|
145
|
+
"toBeDefined",
|
|
146
|
+
"defined",
|
|
147
|
+
`Expected ${this.format(this.actual)} to be defined`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
toBeNaN() {
|
|
151
|
+
const passed = Number.isNaN(this.actual);
|
|
152
|
+
this.assert(passed, "toBeNaN", NaN, `Expected ${this.format(this.actual)} to be NaN`);
|
|
153
|
+
}
|
|
154
|
+
toBeInstanceOf(constructor) {
|
|
155
|
+
const passed = this.actual instanceof constructor;
|
|
156
|
+
this.assert(
|
|
157
|
+
passed,
|
|
158
|
+
"toBeInstanceOf",
|
|
159
|
+
constructor,
|
|
160
|
+
`Expected ${this.format(this.actual)} to be instance of ${constructor.name}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
toBeCloseTo(expected, precision = 2) {
|
|
164
|
+
if (typeof this.actual !== "number" || typeof expected !== "number") {
|
|
165
|
+
throw new AssertionError("Expected both values to be numbers");
|
|
166
|
+
}
|
|
167
|
+
const diff = Math.abs(this.actual - expected);
|
|
168
|
+
const passed = diff < Math.pow(10, -precision) / 2;
|
|
169
|
+
this.assert(
|
|
170
|
+
passed,
|
|
171
|
+
"toBeCloseTo",
|
|
172
|
+
expected,
|
|
173
|
+
`Expected ${this.actual} to be close to ${expected} (precision: ${precision})`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
toMatch(expected) {
|
|
177
|
+
if (typeof this.actual !== "string") {
|
|
178
|
+
throw new AssertionError("Expected value to be a string");
|
|
179
|
+
}
|
|
180
|
+
const regex = typeof expected === "string" ? new RegExp(expected) : expected;
|
|
181
|
+
const passed = regex.test(this.actual);
|
|
182
|
+
this.assert(passed, "toMatch", expected, `Expected "${this.actual}" to match ${expected}`);
|
|
183
|
+
}
|
|
184
|
+
toContain(expected) {
|
|
185
|
+
let passed = false;
|
|
186
|
+
if (typeof this.actual === "string" && typeof expected === "string") {
|
|
187
|
+
passed = this.actual.includes(expected);
|
|
188
|
+
} else if (Array.isArray(this.actual)) {
|
|
189
|
+
passed = this.actual.includes(expected);
|
|
190
|
+
} else if (this.actual && typeof this.actual.has === "function") {
|
|
191
|
+
passed = this.actual.has(expected);
|
|
192
|
+
}
|
|
193
|
+
this.assert(
|
|
194
|
+
passed,
|
|
195
|
+
"toContain",
|
|
196
|
+
expected,
|
|
197
|
+
`Expected ${this.format(this.actual)} to contain ${this.format(expected)}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
toHaveLength(expected) {
|
|
201
|
+
if (this.actual == null || typeof this.actual.length !== "number") {
|
|
202
|
+
throw new AssertionError("Expected value to have a length property");
|
|
203
|
+
}
|
|
204
|
+
const passed = this.actual.length === expected;
|
|
205
|
+
this.assert(
|
|
206
|
+
passed,
|
|
207
|
+
"toHaveLength",
|
|
208
|
+
expected,
|
|
209
|
+
`Expected length ${this.actual.length} to be ${expected}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
toContainEqual(expected) {
|
|
213
|
+
if (!Array.isArray(this.actual)) {
|
|
214
|
+
throw new AssertionError("Expected value to be an array");
|
|
215
|
+
}
|
|
216
|
+
const passed = this.actual.some((item) => deepEqual(item, expected));
|
|
217
|
+
this.assert(
|
|
218
|
+
passed,
|
|
219
|
+
"toContainEqual",
|
|
220
|
+
expected,
|
|
221
|
+
`Expected array to contain equal to ${this.format(expected)}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
toHaveProperty(path, value) {
|
|
225
|
+
const keys = path.split(".");
|
|
226
|
+
let current = this.actual;
|
|
227
|
+
for (const key of keys) {
|
|
228
|
+
if (current == null || !Object.prototype.hasOwnProperty.call(current, key)) {
|
|
229
|
+
this.assert(false, "toHaveProperty", path, `Expected object to have property "${path}"`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
current = current[key];
|
|
233
|
+
}
|
|
234
|
+
if (value !== void 0) {
|
|
235
|
+
const passed = deepEqual(current, value);
|
|
236
|
+
this.assert(
|
|
237
|
+
passed,
|
|
238
|
+
"toHaveProperty",
|
|
239
|
+
value,
|
|
240
|
+
`Expected property "${path}" to have value ${this.format(value)}, got ${this.format(current)}`
|
|
241
|
+
);
|
|
242
|
+
} else {
|
|
243
|
+
this.assert(true, "toHaveProperty", path, "");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
toMatchObject(expected) {
|
|
247
|
+
if (typeof this.actual !== "object" || this.actual === null) {
|
|
248
|
+
throw new AssertionError("Expected value to be an object");
|
|
249
|
+
}
|
|
250
|
+
const passed = Object.keys(expected).every(
|
|
251
|
+
(key) => deepEqual(this.actual[key], expected[key])
|
|
252
|
+
);
|
|
253
|
+
this.assert(
|
|
254
|
+
passed,
|
|
255
|
+
"toMatchObject",
|
|
256
|
+
expected,
|
|
257
|
+
`Expected object to match ${this.format(expected)}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
// DOM-specific matchers
|
|
261
|
+
toBeInTheDocument() {
|
|
262
|
+
if (!(this.actual instanceof Element)) {
|
|
263
|
+
throw new AssertionError("Expected value to be a DOM element");
|
|
264
|
+
}
|
|
265
|
+
const passed = isInDocument(this.actual);
|
|
266
|
+
this.assert(
|
|
267
|
+
passed,
|
|
268
|
+
"toBeInTheDocument",
|
|
269
|
+
"in document",
|
|
270
|
+
"Expected element to be in the document"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
toBeVisible() {
|
|
274
|
+
if (!(this.actual instanceof Element)) {
|
|
275
|
+
throw new AssertionError("Expected value to be a DOM element");
|
|
276
|
+
}
|
|
277
|
+
const passed = isVisible(this.actual);
|
|
278
|
+
this.assert(passed, "toBeVisible", "visible", "Expected element to be visible");
|
|
279
|
+
}
|
|
280
|
+
toBeDisabled() {
|
|
281
|
+
if (!(this.actual instanceof Element)) {
|
|
282
|
+
throw new AssertionError("Expected value to be a DOM element");
|
|
283
|
+
}
|
|
284
|
+
const passed = this.actual.disabled === true || this.actual.hasAttribute("disabled");
|
|
285
|
+
this.assert(passed, "toBeDisabled", "disabled", "Expected element to be disabled");
|
|
286
|
+
}
|
|
287
|
+
toBeEnabled() {
|
|
288
|
+
if (!(this.actual instanceof Element)) {
|
|
289
|
+
throw new AssertionError("Expected value to be a DOM element");
|
|
290
|
+
}
|
|
291
|
+
const passed = this.actual.disabled !== true && !this.actual.hasAttribute("disabled");
|
|
292
|
+
this.assert(passed, "toBeEnabled", "enabled", "Expected element to be enabled");
|
|
293
|
+
}
|
|
294
|
+
toBeChecked() {
|
|
295
|
+
if (!(this.actual instanceof Element)) {
|
|
296
|
+
throw new AssertionError("Expected value to be a DOM element");
|
|
297
|
+
}
|
|
298
|
+
const passed = this.actual.checked === true;
|
|
299
|
+
this.assert(passed, "toBeChecked", "checked", "Expected element to be checked");
|
|
300
|
+
}
|
|
301
|
+
toHaveClass(className) {
|
|
302
|
+
if (!(this.actual instanceof Element)) {
|
|
303
|
+
throw new AssertionError("Expected value to be a DOM element");
|
|
304
|
+
}
|
|
305
|
+
const passed = this.actual.classList.contains(className);
|
|
306
|
+
this.assert(passed, "toHaveClass", className, `Expected element to have class "${className}"`);
|
|
307
|
+
}
|
|
308
|
+
toHaveAttribute(name, value) {
|
|
309
|
+
if (!(this.actual instanceof Element)) {
|
|
310
|
+
throw new AssertionError("Expected value to be a DOM element");
|
|
311
|
+
}
|
|
312
|
+
if (value === void 0) {
|
|
313
|
+
const passed = this.actual.hasAttribute(name);
|
|
314
|
+
this.assert(
|
|
315
|
+
passed,
|
|
316
|
+
"toHaveAttribute",
|
|
317
|
+
`attribute "${name}"`,
|
|
318
|
+
`Expected element to have attribute "${name}"`
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
const passed = this.actual.getAttribute(name) === value;
|
|
322
|
+
this.assert(
|
|
323
|
+
passed,
|
|
324
|
+
"toHaveAttribute",
|
|
325
|
+
`${name}="${value}"`,
|
|
326
|
+
`Expected element to have attribute "${name}" with value "${value}"`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
toHaveTextContent(text) {
|
|
331
|
+
if (!(this.actual instanceof Element)) {
|
|
332
|
+
throw new AssertionError("Expected value to be a DOM element");
|
|
333
|
+
}
|
|
334
|
+
const textContent = this.actual.textContent || "";
|
|
335
|
+
const passed = typeof text === "string" ? textContent === text : text.test(textContent);
|
|
336
|
+
this.assert(
|
|
337
|
+
passed,
|
|
338
|
+
"toHaveTextContent",
|
|
339
|
+
text,
|
|
340
|
+
`Expected element to have text content "${text}"`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
toHaveValue(value) {
|
|
344
|
+
if (!(this.actual instanceof Element)) {
|
|
345
|
+
throw new AssertionError("Expected value to be a DOM element");
|
|
346
|
+
}
|
|
347
|
+
const actualValue = this.actual.value;
|
|
348
|
+
const passed = actualValue === value;
|
|
349
|
+
this.assert(
|
|
350
|
+
passed,
|
|
351
|
+
"toHaveValue",
|
|
352
|
+
value,
|
|
353
|
+
`Expected element to have value "${value}", got "${actualValue}"`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
// HyperScript-specific matchers
|
|
357
|
+
toHaveCompiledScript() {
|
|
358
|
+
const passed = this.actual && typeof this.actual._hyperscript !== "undefined";
|
|
359
|
+
this.assert(
|
|
360
|
+
passed,
|
|
361
|
+
"toHaveCompiledScript",
|
|
362
|
+
"compiled script",
|
|
363
|
+
"Expected element to have compiled hyperscript"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
toHaveExecutedScript() {
|
|
367
|
+
const passed = this.actual && this.actual._hyperscriptExecuted === true;
|
|
368
|
+
this.assert(
|
|
369
|
+
passed,
|
|
370
|
+
"toHaveExecutedScript",
|
|
371
|
+
"executed script",
|
|
372
|
+
"Expected hyperscript to have executed"
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
toHaveTriggeredEvent(eventType) {
|
|
376
|
+
const passed = this.actual && this.actual._triggeredEvents && this.actual._triggeredEvents.includes(eventType);
|
|
377
|
+
this.assert(
|
|
378
|
+
passed,
|
|
379
|
+
"toHaveTriggeredEvent",
|
|
380
|
+
eventType,
|
|
381
|
+
`Expected event "${eventType}" to have been triggered`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
toHaveUpdatedElement() {
|
|
385
|
+
const passed = this.actual && this.actual._wasUpdated === true;
|
|
386
|
+
this.assert(
|
|
387
|
+
passed,
|
|
388
|
+
"toHaveUpdatedElement",
|
|
389
|
+
"updated element",
|
|
390
|
+
"Expected element to have been updated"
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
assert(passed, operator, expected, message) {
|
|
394
|
+
if (this.isNot) {
|
|
395
|
+
passed = !passed;
|
|
396
|
+
message = message.replace("Expected", "Expected NOT");
|
|
397
|
+
}
|
|
398
|
+
if (!passed) {
|
|
399
|
+
const error = new AssertionError(message, expected, this.actual, operator);
|
|
400
|
+
if (typeof this.actual === "object" && typeof expected === "object") {
|
|
401
|
+
error.message += "\n\nDiff:\n" + generateDiff(this.actual, expected);
|
|
402
|
+
}
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
format(value) {
|
|
407
|
+
if (value === null) return "null";
|
|
408
|
+
if (value === void 0) return "undefined";
|
|
409
|
+
if (typeof value === "string") return `"${value}"`;
|
|
410
|
+
if (typeof value === "function") return value.name || "function";
|
|
411
|
+
if (value instanceof Date) return value.toISOString();
|
|
412
|
+
if (value instanceof RegExp) return value.toString();
|
|
413
|
+
if (Array.isArray(value)) return `[${value.map((v) => this.format(v)).join(", ")}]`;
|
|
414
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
415
|
+
return String(value);
|
|
416
|
+
}
|
|
417
|
+
isPromise(value) {
|
|
418
|
+
return value && typeof value.then === "function";
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
function createExpectAPI() {
|
|
422
|
+
return function expect(actual) {
|
|
423
|
+
return new ExpectMatcherImpl(actual);
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
var AssertAPIImpl = class {
|
|
427
|
+
ok(value, message) {
|
|
428
|
+
if (!value) {
|
|
429
|
+
throw new AssertionError(message || `Expected ${value} to be truthy`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
equal(actual, expected, message) {
|
|
433
|
+
if (actual != expected) {
|
|
434
|
+
throw new AssertionError(
|
|
435
|
+
message || `Expected ${actual} to equal ${expected}`,
|
|
436
|
+
expected,
|
|
437
|
+
actual,
|
|
438
|
+
"equal"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
strictEqual(actual, expected, message) {
|
|
443
|
+
if (actual !== expected) {
|
|
444
|
+
throw new AssertionError(
|
|
445
|
+
message || `Expected ${actual} to strictly equal ${expected}`,
|
|
446
|
+
expected,
|
|
447
|
+
actual,
|
|
448
|
+
"strictEqual"
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
notEqual(actual, expected, message) {
|
|
453
|
+
if (actual == expected) {
|
|
454
|
+
throw new AssertionError(
|
|
455
|
+
message || `Expected ${actual} to not equal ${expected}`,
|
|
456
|
+
expected,
|
|
457
|
+
actual,
|
|
458
|
+
"notEqual"
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
notStrictEqual(actual, expected, message) {
|
|
463
|
+
if (actual === expected) {
|
|
464
|
+
throw new AssertionError(
|
|
465
|
+
message || `Expected ${actual} to not strictly equal ${expected}`,
|
|
466
|
+
expected,
|
|
467
|
+
actual,
|
|
468
|
+
"notStrictEqual"
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
throws(fn, error, message) {
|
|
473
|
+
let thrown = false;
|
|
474
|
+
let actualError;
|
|
475
|
+
try {
|
|
476
|
+
fn();
|
|
477
|
+
} catch (e) {
|
|
478
|
+
thrown = true;
|
|
479
|
+
actualError = e;
|
|
480
|
+
}
|
|
481
|
+
if (!thrown) {
|
|
482
|
+
throw new AssertionError(message || "Expected function to throw");
|
|
483
|
+
}
|
|
484
|
+
if (error && !this.matchesError(actualError, error)) {
|
|
485
|
+
throw new AssertionError(
|
|
486
|
+
message || `Expected function to throw ${error}, but got ${actualError}`,
|
|
487
|
+
error,
|
|
488
|
+
actualError,
|
|
489
|
+
"throws"
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
doesNotThrow(fn, message) {
|
|
494
|
+
try {
|
|
495
|
+
fn();
|
|
496
|
+
} catch (e) {
|
|
497
|
+
throw new AssertionError(
|
|
498
|
+
message || `Expected function to not throw, but got: ${e}`,
|
|
499
|
+
"no error",
|
|
500
|
+
e,
|
|
501
|
+
"doesNotThrow"
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
ifError(value) {
|
|
506
|
+
if (value) {
|
|
507
|
+
throw value;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async rejects(promise, error, message) {
|
|
511
|
+
let rejected = false;
|
|
512
|
+
let actualError;
|
|
513
|
+
try {
|
|
514
|
+
await promise;
|
|
515
|
+
} catch (e) {
|
|
516
|
+
rejected = true;
|
|
517
|
+
actualError = e;
|
|
518
|
+
}
|
|
519
|
+
if (!rejected) {
|
|
520
|
+
throw new AssertionError(message || "Expected promise to reject");
|
|
521
|
+
}
|
|
522
|
+
if (error && !this.matchesError(actualError, error)) {
|
|
523
|
+
throw new AssertionError(
|
|
524
|
+
message || `Expected promise to reject with ${error}, but got ${actualError}`,
|
|
525
|
+
error,
|
|
526
|
+
actualError,
|
|
527
|
+
"rejects"
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async doesNotReject(promise, message) {
|
|
532
|
+
try {
|
|
533
|
+
await promise;
|
|
534
|
+
} catch (e) {
|
|
535
|
+
throw new AssertionError(
|
|
536
|
+
message || `Expected promise to not reject, but got: ${e}`,
|
|
537
|
+
"no error",
|
|
538
|
+
e,
|
|
539
|
+
"doesNotReject"
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
elementExists(selector, message) {
|
|
544
|
+
if (typeof document === "undefined") {
|
|
545
|
+
throw new AssertionError("DOM not available");
|
|
546
|
+
}
|
|
547
|
+
const element = document.querySelector(selector);
|
|
548
|
+
if (!element) {
|
|
549
|
+
throw new AssertionError(message || `Expected element with selector "${selector}" to exist`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
elementVisible(selector, message) {
|
|
553
|
+
if (typeof document === "undefined") {
|
|
554
|
+
throw new AssertionError("DOM not available");
|
|
555
|
+
}
|
|
556
|
+
const element = document.querySelector(selector);
|
|
557
|
+
if (!element) {
|
|
558
|
+
throw new AssertionError(`Element with selector "${selector}" does not exist`);
|
|
559
|
+
}
|
|
560
|
+
if (!isVisible(element)) {
|
|
561
|
+
throw new AssertionError(
|
|
562
|
+
message || `Expected element with selector "${selector}" to be visible`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
elementHasClass(selector, className, message) {
|
|
567
|
+
if (typeof document === "undefined") {
|
|
568
|
+
throw new AssertionError("DOM not available");
|
|
569
|
+
}
|
|
570
|
+
const element = document.querySelector(selector);
|
|
571
|
+
if (!element) {
|
|
572
|
+
throw new AssertionError(`Element with selector "${selector}" does not exist`);
|
|
573
|
+
}
|
|
574
|
+
if (!element.classList.contains(className)) {
|
|
575
|
+
throw new AssertionError(
|
|
576
|
+
message || `Expected element with selector "${selector}" to have class "${className}"`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
elementHasText(selector, text, message) {
|
|
581
|
+
if (typeof document === "undefined") {
|
|
582
|
+
throw new AssertionError("DOM not available");
|
|
583
|
+
}
|
|
584
|
+
const element = document.querySelector(selector);
|
|
585
|
+
if (!element) {
|
|
586
|
+
throw new AssertionError(`Element with selector "${selector}" does not exist`);
|
|
587
|
+
}
|
|
588
|
+
if (element.textContent !== text) {
|
|
589
|
+
throw new AssertionError(
|
|
590
|
+
message || `Expected element with selector "${selector}" to have text "${text}"`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
scriptCompiled(script, message) {
|
|
595
|
+
throw new AssertionError(message || "Script compilation assertion not implemented");
|
|
596
|
+
}
|
|
597
|
+
scriptExecuted(script, message) {
|
|
598
|
+
throw new AssertionError(message || "Script execution assertion not implemented");
|
|
599
|
+
}
|
|
600
|
+
eventTriggered(eventType, message) {
|
|
601
|
+
throw new AssertionError(message || "Event trigger assertion not implemented");
|
|
602
|
+
}
|
|
603
|
+
matchesError(actual, expected) {
|
|
604
|
+
if (typeof expected === "string") {
|
|
605
|
+
return actual.message === expected;
|
|
606
|
+
}
|
|
607
|
+
if (expected instanceof RegExp) {
|
|
608
|
+
return expected.test(actual.message);
|
|
609
|
+
}
|
|
610
|
+
if (typeof expected === "function") {
|
|
611
|
+
return actual instanceof expected;
|
|
612
|
+
}
|
|
613
|
+
return actual === expected;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
function createAssertAPI() {
|
|
617
|
+
return new AssertAPIImpl();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/runner.ts
|
|
621
|
+
var CoreTestRunner = class {
|
|
622
|
+
constructor() {
|
|
623
|
+
this.executionContext = null;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Run all test suites
|
|
627
|
+
*/
|
|
628
|
+
async run(suites, config) {
|
|
629
|
+
const results = [];
|
|
630
|
+
this.executionContext = await this.setupExecutionContext(config);
|
|
631
|
+
try {
|
|
632
|
+
await this.runSetupFiles(config.setupFiles);
|
|
633
|
+
for (const suite of suites) {
|
|
634
|
+
const suiteResults = await this.runSuite(suite, config);
|
|
635
|
+
results.push(...suiteResults);
|
|
636
|
+
}
|
|
637
|
+
await this.runTeardownFiles(config.teardownFiles);
|
|
638
|
+
} finally {
|
|
639
|
+
await this.cleanupExecutionContext();
|
|
640
|
+
}
|
|
641
|
+
return results;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Run a single test suite
|
|
645
|
+
*/
|
|
646
|
+
async runSuite(suite, config) {
|
|
647
|
+
const results = [];
|
|
648
|
+
if (suite.skip) {
|
|
649
|
+
for (const test2 of suite.tests) {
|
|
650
|
+
results.push(this.createSkippedResult(suite.name, test2.name));
|
|
651
|
+
}
|
|
652
|
+
return results;
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
if (suite.setup) {
|
|
656
|
+
await suite.setup();
|
|
657
|
+
}
|
|
658
|
+
for (const test2 of suite.tests) {
|
|
659
|
+
const result = await this.runTest(test2, this.createTestContext(suite, test2, config));
|
|
660
|
+
results.push(result);
|
|
661
|
+
}
|
|
662
|
+
for (const nestedSuite of suite.suites) {
|
|
663
|
+
const nestedResults = await this.runSuite(nestedSuite, config);
|
|
664
|
+
results.push(...nestedResults);
|
|
665
|
+
}
|
|
666
|
+
} catch (error) {
|
|
667
|
+
const testError = this.formatError(error);
|
|
668
|
+
for (const test2 of suite.tests) {
|
|
669
|
+
results.push({
|
|
670
|
+
suite: suite.name,
|
|
671
|
+
test: test2.name,
|
|
672
|
+
status: "failed",
|
|
673
|
+
duration: 0,
|
|
674
|
+
error: testError,
|
|
675
|
+
logs: [],
|
|
676
|
+
screenshots: []
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
} finally {
|
|
680
|
+
try {
|
|
681
|
+
if (suite.teardown) {
|
|
682
|
+
await suite.teardown();
|
|
683
|
+
}
|
|
684
|
+
} catch (error) {
|
|
685
|
+
console.error("Suite teardown failed:", error);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return results;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Run a single test
|
|
692
|
+
*/
|
|
693
|
+
async runTest(test2, context) {
|
|
694
|
+
const startTime = Date.now();
|
|
695
|
+
if (test2.skip) {
|
|
696
|
+
return this.createSkippedResult(context.suite, context.test);
|
|
697
|
+
}
|
|
698
|
+
let status = "running";
|
|
699
|
+
let error;
|
|
700
|
+
const logs = [];
|
|
701
|
+
const screenshots = [];
|
|
702
|
+
try {
|
|
703
|
+
await this.setupTestFixtures(test2, context);
|
|
704
|
+
await this.runBeforeEachHooks(context);
|
|
705
|
+
const timeout = test2.timeout || this.executionContext?.config.timeout || 5e3;
|
|
706
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
707
|
+
setTimeout(() => reject(new Error(`Test timeout after ${timeout}ms`)), timeout);
|
|
708
|
+
});
|
|
709
|
+
await Promise.race([test2.fn(context), timeoutPromise]);
|
|
710
|
+
status = "passed";
|
|
711
|
+
} catch (err) {
|
|
712
|
+
status = "failed";
|
|
713
|
+
error = this.formatError(err);
|
|
714
|
+
if (this.executionContext?.page && this.executionContext.config.environment === "browser") {
|
|
715
|
+
try {
|
|
716
|
+
const screenshotPath = await this.takeScreenshot(context.suite, context.test);
|
|
717
|
+
if (screenshotPath) {
|
|
718
|
+
screenshots.push(screenshotPath);
|
|
719
|
+
}
|
|
720
|
+
} catch (screenshotError) {
|
|
721
|
+
console.warn("Failed to take screenshot:", screenshotError);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
} finally {
|
|
725
|
+
try {
|
|
726
|
+
await this.runAfterEachHooks(context);
|
|
727
|
+
await this.cleanupTestFixtures(test2, context);
|
|
728
|
+
} catch (cleanupError) {
|
|
729
|
+
console.error("Test cleanup failed:", cleanupError);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const duration = Date.now() - startTime;
|
|
733
|
+
return {
|
|
734
|
+
suite: context.suite,
|
|
735
|
+
test: context.test,
|
|
736
|
+
status,
|
|
737
|
+
duration,
|
|
738
|
+
error,
|
|
739
|
+
logs: [...logs, ...this.executionContext?.logs || []],
|
|
740
|
+
screenshots
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Setup execution context
|
|
745
|
+
*/
|
|
746
|
+
async setupExecutionContext(config) {
|
|
747
|
+
const context = {
|
|
748
|
+
config,
|
|
749
|
+
logs: [],
|
|
750
|
+
screenshots: []
|
|
751
|
+
};
|
|
752
|
+
switch (config.environment) {
|
|
753
|
+
case "jsdom":
|
|
754
|
+
await this.setupJSDOMContext(context);
|
|
755
|
+
break;
|
|
756
|
+
case "browser":
|
|
757
|
+
case "playwright":
|
|
758
|
+
await this.setupBrowserContext(context);
|
|
759
|
+
break;
|
|
760
|
+
case "puppeteer":
|
|
761
|
+
await this.setupPuppeteerContext(context);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
return context;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Setup JSDOM context
|
|
768
|
+
*/
|
|
769
|
+
async setupJSDOMContext(context) {
|
|
770
|
+
const { config } = context;
|
|
771
|
+
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
|
|
772
|
+
url: config.baseURL || "http://localhost",
|
|
773
|
+
contentType: "text/html",
|
|
774
|
+
pretendToBeVisual: true,
|
|
775
|
+
resources: "usable"
|
|
776
|
+
});
|
|
777
|
+
global.window = dom.window;
|
|
778
|
+
global.document = dom.window.document;
|
|
779
|
+
global.navigator = dom.window.navigator;
|
|
780
|
+
global.location = dom.window.location;
|
|
781
|
+
global.HTMLElement = dom.window.HTMLElement;
|
|
782
|
+
global.Event = dom.window.Event;
|
|
783
|
+
global.CustomEvent = dom.window.CustomEvent;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Setup browser context (Playwright)
|
|
787
|
+
*/
|
|
788
|
+
async setupBrowserContext(context) {
|
|
789
|
+
try {
|
|
790
|
+
const { chromium, firefox, webkit } = await import('playwright');
|
|
791
|
+
const { config } = context;
|
|
792
|
+
let browserEngine;
|
|
793
|
+
switch (config.browser) {
|
|
794
|
+
case "firefox":
|
|
795
|
+
browserEngine = firefox;
|
|
796
|
+
break;
|
|
797
|
+
case "webkit":
|
|
798
|
+
browserEngine = webkit;
|
|
799
|
+
break;
|
|
800
|
+
default:
|
|
801
|
+
browserEngine = chromium;
|
|
802
|
+
}
|
|
803
|
+
const browser = await browserEngine.launch({
|
|
804
|
+
headless: config.headless
|
|
805
|
+
});
|
|
806
|
+
context.browser = browser;
|
|
807
|
+
const browserContext = await browser.newContext({
|
|
808
|
+
...config.viewport && { viewport: config.viewport },
|
|
809
|
+
...config.baseURL && { baseURL: config.baseURL }
|
|
810
|
+
});
|
|
811
|
+
context.page = await browserContext.newPage();
|
|
812
|
+
context.page.on("console", (msg) => {
|
|
813
|
+
context.logs.push({
|
|
814
|
+
level: msg.type(),
|
|
815
|
+
message: msg.text(),
|
|
816
|
+
timestamp: Date.now(),
|
|
817
|
+
source: "browser"
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
context.page.on("pageerror", (error) => {
|
|
821
|
+
context.logs.push({
|
|
822
|
+
level: "error",
|
|
823
|
+
message: error.message,
|
|
824
|
+
timestamp: Date.now(),
|
|
825
|
+
source: "browser"
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
} catch (error) {
|
|
829
|
+
throw new Error(`Failed to setup browser context: ${error}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Setup Puppeteer context
|
|
834
|
+
*/
|
|
835
|
+
async setupPuppeteerContext(context) {
|
|
836
|
+
try {
|
|
837
|
+
const puppeteer = await import('puppeteer');
|
|
838
|
+
const { config } = context;
|
|
839
|
+
const browser = await puppeteer.default.launch({
|
|
840
|
+
headless: config.headless,
|
|
841
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
842
|
+
});
|
|
843
|
+
context.browser = browser;
|
|
844
|
+
context.page = await browser.newPage();
|
|
845
|
+
await context.page.setViewport(config.viewport);
|
|
846
|
+
if (config.baseURL) {
|
|
847
|
+
await context.page.goto(config.baseURL);
|
|
848
|
+
}
|
|
849
|
+
context.page.on("console", (msg) => {
|
|
850
|
+
context.logs.push({
|
|
851
|
+
level: msg.type(),
|
|
852
|
+
message: msg.text(),
|
|
853
|
+
timestamp: Date.now(),
|
|
854
|
+
source: "browser"
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
context.page.on("pageerror", (error) => {
|
|
858
|
+
context.logs.push({
|
|
859
|
+
level: "error",
|
|
860
|
+
message: error.message,
|
|
861
|
+
timestamp: Date.now(),
|
|
862
|
+
source: "browser"
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
} catch (error) {
|
|
866
|
+
throw new Error(`Failed to setup Puppeteer context: ${error}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Create test context
|
|
871
|
+
*/
|
|
872
|
+
createTestContext(suite, test2, config) {
|
|
873
|
+
const context = {
|
|
874
|
+
suite: suite.name,
|
|
875
|
+
test: test2.name,
|
|
876
|
+
environment: config.environment,
|
|
877
|
+
fixtures: { ...config.fixtures, ...test2.fixtures },
|
|
878
|
+
expect: createExpectAPI(),
|
|
879
|
+
assert: createAssertAPI(),
|
|
880
|
+
skip: () => {
|
|
881
|
+
throw new Error("SKIP_TEST");
|
|
882
|
+
},
|
|
883
|
+
fail: (message) => {
|
|
884
|
+
throw new Error(message);
|
|
885
|
+
},
|
|
886
|
+
timeout: (ms) => {
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
if (this.executionContext?.page) {
|
|
890
|
+
context.page = this.executionContext.page;
|
|
891
|
+
context.browser = this.executionContext.browser;
|
|
892
|
+
}
|
|
893
|
+
if (config.environment === "jsdom") {
|
|
894
|
+
context.document = global.document;
|
|
895
|
+
context.window = global.window;
|
|
896
|
+
}
|
|
897
|
+
return context;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Setup test fixtures
|
|
901
|
+
*/
|
|
902
|
+
async setupTestFixtures(test2, context) {
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Cleanup test fixtures
|
|
906
|
+
*/
|
|
907
|
+
async cleanupTestFixtures(test2, context) {
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Run beforeEach hooks
|
|
911
|
+
*/
|
|
912
|
+
async runBeforeEachHooks(context) {
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Run afterEach hooks
|
|
916
|
+
*/
|
|
917
|
+
async runAfterEachHooks(context) {
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Run setup files
|
|
921
|
+
*/
|
|
922
|
+
async runSetupFiles(setupFiles) {
|
|
923
|
+
for (const file of setupFiles) {
|
|
924
|
+
try {
|
|
925
|
+
await import(file);
|
|
926
|
+
} catch (error) {
|
|
927
|
+
console.error(`Failed to run setup file ${file}:`, error);
|
|
928
|
+
throw error;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Run teardown files
|
|
934
|
+
*/
|
|
935
|
+
async runTeardownFiles(teardownFiles) {
|
|
936
|
+
for (const file of teardownFiles) {
|
|
937
|
+
try {
|
|
938
|
+
await import(file);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
console.error(`Failed to run teardown file ${file}:`, error);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Take screenshot
|
|
946
|
+
*/
|
|
947
|
+
async takeScreenshot(suite, test2) {
|
|
948
|
+
if (!this.executionContext?.page) return null;
|
|
949
|
+
try {
|
|
950
|
+
const filename = `${suite}-${test2}-${Date.now()}.png`;
|
|
951
|
+
const path = `./screenshots/${filename}`;
|
|
952
|
+
await this.executionContext.page.screenshot({
|
|
953
|
+
path,
|
|
954
|
+
fullPage: true
|
|
955
|
+
});
|
|
956
|
+
return path;
|
|
957
|
+
} catch (error) {
|
|
958
|
+
console.error("Screenshot failed:", error);
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Format error for test result
|
|
964
|
+
*/
|
|
965
|
+
formatError(error) {
|
|
966
|
+
if (error instanceof Error) {
|
|
967
|
+
return {
|
|
968
|
+
name: error.name,
|
|
969
|
+
message: error.message,
|
|
970
|
+
stack: error.stack
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
name: "Error",
|
|
975
|
+
message: String(error)
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Create skipped test result
|
|
980
|
+
*/
|
|
981
|
+
createSkippedResult(suite, test2) {
|
|
982
|
+
return {
|
|
983
|
+
suite,
|
|
984
|
+
test: test2,
|
|
985
|
+
status: "skipped",
|
|
986
|
+
duration: 0,
|
|
987
|
+
logs: [],
|
|
988
|
+
screenshots: []
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Cleanup execution context
|
|
993
|
+
*/
|
|
994
|
+
async cleanupExecutionContext() {
|
|
995
|
+
if (!this.executionContext) return;
|
|
996
|
+
try {
|
|
997
|
+
if (this.executionContext.browser) {
|
|
998
|
+
await this.executionContext.browser.close();
|
|
999
|
+
}
|
|
1000
|
+
if (this.executionContext.config.environment === "jsdom") {
|
|
1001
|
+
delete global.window;
|
|
1002
|
+
delete global.document;
|
|
1003
|
+
delete global.navigator;
|
|
1004
|
+
delete global.location;
|
|
1005
|
+
delete global.HTMLElement;
|
|
1006
|
+
delete global.Event;
|
|
1007
|
+
delete global.CustomEvent;
|
|
1008
|
+
}
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
console.error("Cleanup failed:", error);
|
|
1011
|
+
} finally {
|
|
1012
|
+
this.executionContext = null;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
var ParallelTestRunner = class extends CoreTestRunner {
|
|
1017
|
+
/**
|
|
1018
|
+
* Run test suites in parallel
|
|
1019
|
+
*/
|
|
1020
|
+
async run(suites, config) {
|
|
1021
|
+
if (!config.parallel || config.maxWorkers <= 1) {
|
|
1022
|
+
return super.run(suites, config);
|
|
1023
|
+
}
|
|
1024
|
+
const workers = Math.min(config.maxWorkers, suites.length);
|
|
1025
|
+
const chunks = this.chunkSuites(suites, workers);
|
|
1026
|
+
const workerPromises = chunks.map((chunk) => this.runWorker(chunk, config));
|
|
1027
|
+
const workerResults = await Promise.all(workerPromises);
|
|
1028
|
+
return workerResults.flat();
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Split suites into chunks for parallel execution
|
|
1032
|
+
*/
|
|
1033
|
+
chunkSuites(suites, chunkCount) {
|
|
1034
|
+
const chunks = [];
|
|
1035
|
+
const chunkSize = Math.ceil(suites.length / chunkCount);
|
|
1036
|
+
for (let i = 0; i < suites.length; i += chunkSize) {
|
|
1037
|
+
chunks.push(suites.slice(i, i + chunkSize));
|
|
1038
|
+
}
|
|
1039
|
+
return chunks;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Run a worker with assigned test suites
|
|
1043
|
+
*/
|
|
1044
|
+
async runWorker(suites, config) {
|
|
1045
|
+
const worker = new CoreTestRunner();
|
|
1046
|
+
return worker.run(suites, config);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
function createTestRunner(config) {
|
|
1050
|
+
if (config.parallel && config.maxWorkers > 1) {
|
|
1051
|
+
return new ParallelTestRunner();
|
|
1052
|
+
}
|
|
1053
|
+
return new CoreTestRunner();
|
|
1054
|
+
}
|
|
1055
|
+
async function measurePerformance(page, testFn) {
|
|
1056
|
+
await page.evaluate(() => {
|
|
1057
|
+
performance.clearMarks();
|
|
1058
|
+
performance.clearMeasures();
|
|
1059
|
+
});
|
|
1060
|
+
const startTime = Date.now();
|
|
1061
|
+
await testFn();
|
|
1062
|
+
const metrics = await page.evaluate(() => {
|
|
1063
|
+
const navigation = performance.getEntriesByType("navigation")[0];
|
|
1064
|
+
const paint = performance.getEntriesByType("paint");
|
|
1065
|
+
const firstPaint = paint.find((entry) => entry.name === "first-paint")?.startTime || 0;
|
|
1066
|
+
const firstContentfulPaint = paint.find((entry) => entry.name === "first-contentful-paint")?.startTime || 0;
|
|
1067
|
+
return {
|
|
1068
|
+
loadTime: navigation ? navigation.loadEventEnd - navigation.startTime : 0,
|
|
1069
|
+
domContentLoaded: navigation ? navigation.domContentLoadedEventEnd - navigation.startTime : 0,
|
|
1070
|
+
firstPaint,
|
|
1071
|
+
firstContentfulPaint,
|
|
1072
|
+
largestContentfulPaint: 0,
|
|
1073
|
+
// Would need additional measurement
|
|
1074
|
+
cumulativeLayoutShift: 0,
|
|
1075
|
+
// Would need additional measurement
|
|
1076
|
+
firstInputDelay: 0,
|
|
1077
|
+
// Would need additional measurement
|
|
1078
|
+
memoryUsage: performance.memory ? {
|
|
1079
|
+
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
|
1080
|
+
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
|
1081
|
+
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
|
|
1082
|
+
} : {
|
|
1083
|
+
usedJSHeapSize: 0,
|
|
1084
|
+
totalJSHeapSize: 0,
|
|
1085
|
+
jsHeapSizeLimit: 0
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
});
|
|
1089
|
+
return {
|
|
1090
|
+
...metrics,
|
|
1091
|
+
loadTime: metrics.loadTime || Date.now() - startTime
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/index.ts
|
|
1096
|
+
var testSuites = [];
|
|
1097
|
+
var currentSuite = null;
|
|
1098
|
+
var DEFAULT_CONFIG = {
|
|
1099
|
+
environment: "jsdom",
|
|
1100
|
+
browser: "chromium",
|
|
1101
|
+
timeout: 5e3,
|
|
1102
|
+
retries: 0,
|
|
1103
|
+
parallel: false,
|
|
1104
|
+
maxWorkers: 1,
|
|
1105
|
+
headless: true,
|
|
1106
|
+
viewport: {
|
|
1107
|
+
width: 1280,
|
|
1108
|
+
height: 720
|
|
1109
|
+
},
|
|
1110
|
+
setupFiles: [],
|
|
1111
|
+
teardownFiles: [],
|
|
1112
|
+
reporters: [],
|
|
1113
|
+
coverage: {
|
|
1114
|
+
enabled: false,
|
|
1115
|
+
threshold: 80,
|
|
1116
|
+
include: ["src/**/*"],
|
|
1117
|
+
exclude: ["**/*.test.*", "**/*.spec.*"]
|
|
1118
|
+
},
|
|
1119
|
+
fixtures: {}
|
|
1120
|
+
};
|
|
1121
|
+
function describe(name, optionsOrFn, fn) {
|
|
1122
|
+
const actualOptions = typeof optionsOrFn === "function" ? {} : optionsOrFn;
|
|
1123
|
+
const actualFn = typeof optionsOrFn === "function" ? optionsOrFn : fn;
|
|
1124
|
+
const suite = {
|
|
1125
|
+
name,
|
|
1126
|
+
...actualOptions.description !== void 0 ? { description: actualOptions.description } : {},
|
|
1127
|
+
...actualOptions.setup !== void 0 ? { setup: actualOptions.setup } : {},
|
|
1128
|
+
...actualOptions.teardown !== void 0 ? { teardown: actualOptions.teardown } : {},
|
|
1129
|
+
...actualOptions.beforeEach !== void 0 ? { beforeEach: actualOptions.beforeEach } : {},
|
|
1130
|
+
...actualOptions.afterEach !== void 0 ? { afterEach: actualOptions.afterEach } : {},
|
|
1131
|
+
tests: [],
|
|
1132
|
+
suites: [],
|
|
1133
|
+
...actualOptions.timeout !== void 0 ? { timeout: actualOptions.timeout } : {},
|
|
1134
|
+
...actualOptions.retries !== void 0 ? { retries: actualOptions.retries } : {},
|
|
1135
|
+
...actualOptions.skip !== void 0 ? { skip: actualOptions.skip } : {},
|
|
1136
|
+
...actualOptions.only !== void 0 ? { only: actualOptions.only } : {}
|
|
1137
|
+
};
|
|
1138
|
+
const parentSuite = currentSuite;
|
|
1139
|
+
if (parentSuite) {
|
|
1140
|
+
parentSuite.suites.push(suite);
|
|
1141
|
+
} else {
|
|
1142
|
+
testSuites.push(suite);
|
|
1143
|
+
}
|
|
1144
|
+
currentSuite = suite;
|
|
1145
|
+
actualFn();
|
|
1146
|
+
currentSuite = parentSuite;
|
|
1147
|
+
}
|
|
1148
|
+
function it(name, optionsOrFn, fn) {
|
|
1149
|
+
const actualOptions = typeof optionsOrFn === "function" ? {} : optionsOrFn;
|
|
1150
|
+
const actualFn = typeof optionsOrFn === "function" ? optionsOrFn : fn;
|
|
1151
|
+
if (!currentSuite) {
|
|
1152
|
+
throw new Error("Test case must be defined within a test suite (describe block)");
|
|
1153
|
+
}
|
|
1154
|
+
const test2 = {
|
|
1155
|
+
name,
|
|
1156
|
+
...actualOptions.description !== void 0 ? { description: actualOptions.description } : {},
|
|
1157
|
+
fn: actualFn,
|
|
1158
|
+
...actualOptions.timeout !== void 0 ? { timeout: actualOptions.timeout } : {},
|
|
1159
|
+
...actualOptions.retries !== void 0 ? { retries: actualOptions.retries } : {},
|
|
1160
|
+
...actualOptions.skip !== void 0 ? { skip: actualOptions.skip } : {},
|
|
1161
|
+
...actualOptions.only !== void 0 ? { only: actualOptions.only } : {},
|
|
1162
|
+
tags: actualOptions.tags || [],
|
|
1163
|
+
fixtures: actualOptions.fixtures || {}
|
|
1164
|
+
};
|
|
1165
|
+
currentSuite.tests.push(test2);
|
|
1166
|
+
}
|
|
1167
|
+
var test = it;
|
|
1168
|
+
function describe_skip(name, fn) {
|
|
1169
|
+
describe(name, { skip: true }, fn);
|
|
1170
|
+
}
|
|
1171
|
+
function it_skip(name, fn) {
|
|
1172
|
+
it(name, { skip: true }, fn);
|
|
1173
|
+
}
|
|
1174
|
+
function describe_only(name, fn) {
|
|
1175
|
+
describe(name, { only: true }, fn);
|
|
1176
|
+
}
|
|
1177
|
+
function it_only(name, fn) {
|
|
1178
|
+
it(name, { only: true }, fn);
|
|
1179
|
+
}
|
|
1180
|
+
function beforeAll(fn) {
|
|
1181
|
+
if (!currentSuite) {
|
|
1182
|
+
throw new Error("beforeAll must be called within a test suite");
|
|
1183
|
+
}
|
|
1184
|
+
currentSuite.setup = fn;
|
|
1185
|
+
}
|
|
1186
|
+
function afterAll(fn) {
|
|
1187
|
+
if (!currentSuite) {
|
|
1188
|
+
throw new Error("afterAll must be called within a test suite");
|
|
1189
|
+
}
|
|
1190
|
+
currentSuite.teardown = fn;
|
|
1191
|
+
}
|
|
1192
|
+
function beforeEach(fn) {
|
|
1193
|
+
if (!currentSuite) {
|
|
1194
|
+
throw new Error("beforeEach must be called within a test suite");
|
|
1195
|
+
}
|
|
1196
|
+
currentSuite.beforeEach = fn;
|
|
1197
|
+
}
|
|
1198
|
+
function afterEach(fn) {
|
|
1199
|
+
if (!currentSuite) {
|
|
1200
|
+
throw new Error("afterEach must be called within a test suite");
|
|
1201
|
+
}
|
|
1202
|
+
currentSuite.afterEach = fn;
|
|
1203
|
+
}
|
|
1204
|
+
async function runTests(config = {}) {
|
|
1205
|
+
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
1206
|
+
const runner = createTestRunner(finalConfig);
|
|
1207
|
+
let suitesToRun = testSuites;
|
|
1208
|
+
const onlySuites = testSuites.filter((suite) => suite.only);
|
|
1209
|
+
if (onlySuites.length > 0) {
|
|
1210
|
+
suitesToRun = onlySuites;
|
|
1211
|
+
}
|
|
1212
|
+
return runner.run(suitesToRun, finalConfig);
|
|
1213
|
+
}
|
|
1214
|
+
function clearTests() {
|
|
1215
|
+
testSuites.length = 0;
|
|
1216
|
+
currentSuite = null;
|
|
1217
|
+
}
|
|
1218
|
+
function fixture(name, setup, teardown) {
|
|
1219
|
+
return {
|
|
1220
|
+
name,
|
|
1221
|
+
async create() {
|
|
1222
|
+
const instance = await setup();
|
|
1223
|
+
if (teardown) {
|
|
1224
|
+
instance.__teardown = () => teardown(instance);
|
|
1225
|
+
}
|
|
1226
|
+
return instance;
|
|
1227
|
+
},
|
|
1228
|
+
async destroy(instance) {
|
|
1229
|
+
if (instance.__teardown) {
|
|
1230
|
+
await instance.__teardown();
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
function createMock(implementation) {
|
|
1236
|
+
const calls = [];
|
|
1237
|
+
const results = [];
|
|
1238
|
+
let mockImplementation = implementation;
|
|
1239
|
+
const mockFn = function(...args) {
|
|
1240
|
+
calls.push(args);
|
|
1241
|
+
try {
|
|
1242
|
+
if (mockImplementation) {
|
|
1243
|
+
const result2 = mockImplementation(...args);
|
|
1244
|
+
results.push({ type: "return", value: result2 });
|
|
1245
|
+
return result2;
|
|
1246
|
+
}
|
|
1247
|
+
const result = void 0;
|
|
1248
|
+
results.push({ type: "return", value: result });
|
|
1249
|
+
return result;
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
results.push({ type: "throw", value: error });
|
|
1252
|
+
throw error;
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
mockFn.mockReturnValue = (value) => {
|
|
1256
|
+
mockImplementation = () => value;
|
|
1257
|
+
return mockFn;
|
|
1258
|
+
};
|
|
1259
|
+
mockFn.mockReturnValueOnce = (value) => {
|
|
1260
|
+
const originalImpl = mockImplementation;
|
|
1261
|
+
let called = false;
|
|
1262
|
+
mockImplementation = (...args) => {
|
|
1263
|
+
if (!called) {
|
|
1264
|
+
called = true;
|
|
1265
|
+
return value;
|
|
1266
|
+
}
|
|
1267
|
+
return originalImpl ? originalImpl(...args) : void 0;
|
|
1268
|
+
};
|
|
1269
|
+
return mockFn;
|
|
1270
|
+
};
|
|
1271
|
+
mockFn.mockResolvedValue = (value) => {
|
|
1272
|
+
mockImplementation = () => Promise.resolve(value);
|
|
1273
|
+
return mockFn;
|
|
1274
|
+
};
|
|
1275
|
+
mockFn.mockRejectedValue = (error) => {
|
|
1276
|
+
mockImplementation = () => Promise.reject(error);
|
|
1277
|
+
return mockFn;
|
|
1278
|
+
};
|
|
1279
|
+
mockFn.mockImplementation = (fn) => {
|
|
1280
|
+
mockImplementation = fn;
|
|
1281
|
+
return mockFn;
|
|
1282
|
+
};
|
|
1283
|
+
mockFn.mockClear = () => {
|
|
1284
|
+
calls.length = 0;
|
|
1285
|
+
results.length = 0;
|
|
1286
|
+
return mockFn;
|
|
1287
|
+
};
|
|
1288
|
+
mockFn.mockReset = () => {
|
|
1289
|
+
calls.length = 0;
|
|
1290
|
+
results.length = 0;
|
|
1291
|
+
mockImplementation = void 0;
|
|
1292
|
+
return mockFn;
|
|
1293
|
+
};
|
|
1294
|
+
mockFn.mockRestore = () => {
|
|
1295
|
+
calls.length = 0;
|
|
1296
|
+
results.length = 0;
|
|
1297
|
+
mockImplementation = implementation;
|
|
1298
|
+
return mockFn;
|
|
1299
|
+
};
|
|
1300
|
+
Object.defineProperty(mockFn, "calls", {
|
|
1301
|
+
get: () => calls
|
|
1302
|
+
});
|
|
1303
|
+
Object.defineProperty(mockFn, "results", {
|
|
1304
|
+
get: () => results
|
|
1305
|
+
});
|
|
1306
|
+
return mockFn;
|
|
1307
|
+
}
|
|
1308
|
+
function createSpy(target, method) {
|
|
1309
|
+
const original = target[method];
|
|
1310
|
+
const spy = createMock(original);
|
|
1311
|
+
spy.original = original;
|
|
1312
|
+
target[method] = spy;
|
|
1313
|
+
const originalRestore = spy.mockRestore;
|
|
1314
|
+
spy.mockRestore = () => {
|
|
1315
|
+
target[method] = original;
|
|
1316
|
+
return originalRestore();
|
|
1317
|
+
};
|
|
1318
|
+
return spy;
|
|
1319
|
+
}
|
|
1320
|
+
async function waitFor(condition, options = {}) {
|
|
1321
|
+
const { timeout = 5e3, interval = 100 } = options;
|
|
1322
|
+
const startTime = Date.now();
|
|
1323
|
+
while (Date.now() - startTime < timeout) {
|
|
1324
|
+
if (await condition()) {
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
1328
|
+
}
|
|
1329
|
+
throw new Error(`Condition not met within ${timeout}ms`);
|
|
1330
|
+
}
|
|
1331
|
+
function createPageObject(definition) {
|
|
1332
|
+
return {
|
|
1333
|
+
...definition,
|
|
1334
|
+
async goto(page) {
|
|
1335
|
+
if (definition.url) {
|
|
1336
|
+
await page.goto(definition.url);
|
|
1337
|
+
}
|
|
1338
|
+
},
|
|
1339
|
+
element(name) {
|
|
1340
|
+
const selector = definition.selectors[name];
|
|
1341
|
+
if (!selector) {
|
|
1342
|
+
throw new Error(`Selector "${name}" not found in page object "${definition.name}"`);
|
|
1343
|
+
}
|
|
1344
|
+
return selector;
|
|
1345
|
+
},
|
|
1346
|
+
async action(page, name, ...args) {
|
|
1347
|
+
const action = definition.actions[name];
|
|
1348
|
+
if (!action) {
|
|
1349
|
+
throw new Error(`Action "${name}" not found in page object "${definition.name}"`);
|
|
1350
|
+
}
|
|
1351
|
+
return action.call(this, page, ...args);
|
|
1352
|
+
},
|
|
1353
|
+
async assert(page, name, ...args) {
|
|
1354
|
+
const assertion = definition.assertions[name];
|
|
1355
|
+
if (!assertion) {
|
|
1356
|
+
throw new Error(`Assertion "${name}" not found in page object "${definition.name}"`);
|
|
1357
|
+
}
|
|
1358
|
+
return assertion.call(this, page, ...args);
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
function createLokaScriptTestContext(hyperfixi) {
|
|
1363
|
+
return {
|
|
1364
|
+
// Compile and test hyperscript
|
|
1365
|
+
async compileAndTest(script, element, event) {
|
|
1366
|
+
const compiled = await hyperfixi.compile(script);
|
|
1367
|
+
if (element && compiled) {
|
|
1368
|
+
element.setAttribute("data-compiled-script", compiled);
|
|
1369
|
+
if (event) {
|
|
1370
|
+
element.dispatchEvent(new Event(event));
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return compiled;
|
|
1374
|
+
},
|
|
1375
|
+
// Test script compilation
|
|
1376
|
+
async testCompilation(script) {
|
|
1377
|
+
try {
|
|
1378
|
+
const result = await hyperfixi.compile(script);
|
|
1379
|
+
return { success: true, result };
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
return { success: false, error };
|
|
1382
|
+
}
|
|
1383
|
+
},
|
|
1384
|
+
// Test script execution
|
|
1385
|
+
async testExecution(script, element, eventType = "click") {
|
|
1386
|
+
const compiled = await hyperfixi.compile(script);
|
|
1387
|
+
if (compiled) {
|
|
1388
|
+
let executed = false;
|
|
1389
|
+
const originalConsoleLog = console.log;
|
|
1390
|
+
console.log = (...args) => {
|
|
1391
|
+
if (args.includes("HYPERSCRIPT_EXECUTED")) {
|
|
1392
|
+
executed = true;
|
|
1393
|
+
}
|
|
1394
|
+
originalConsoleLog(...args);
|
|
1395
|
+
};
|
|
1396
|
+
try {
|
|
1397
|
+
element.setAttribute("data-hyperscript", script);
|
|
1398
|
+
element.dispatchEvent(new Event(eventType));
|
|
1399
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1400
|
+
return { executed, compiled };
|
|
1401
|
+
} finally {
|
|
1402
|
+
console.log = originalConsoleLog;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return { executed: false, compiled: null };
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
async function quickStartTesting(options = {}) {
|
|
1410
|
+
const config = {
|
|
1411
|
+
environment: options.environment || "jsdom",
|
|
1412
|
+
browser: options.browser || "chromium",
|
|
1413
|
+
headless: options.headless !== false,
|
|
1414
|
+
...options.baseURL !== void 0 ? { baseURL: options.baseURL } : {}
|
|
1415
|
+
};
|
|
1416
|
+
if (options.testFiles) {
|
|
1417
|
+
for (const file of options.testFiles) {
|
|
1418
|
+
try {
|
|
1419
|
+
await import(file);
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
console.error(`Failed to load test file ${file}:`, error);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
return {
|
|
1426
|
+
config,
|
|
1427
|
+
run: () => runTests(config),
|
|
1428
|
+
clear: clearTests
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
var VERSION = "0.1.0";
|
|
1432
|
+
|
|
1433
|
+
export { AssertionError, CoreTestRunner, DEFAULT_CONFIG, ParallelTestRunner, VERSION, afterAll, afterEach, beforeAll, beforeEach, clearTests, createAssertAPI, createExpectAPI, createLokaScriptTestContext, createMock, createPageObject, createSpy, createTestRunner, describe, describe_only, describe_skip, fixture, it, it_only, it_skip, measurePerformance, quickStartTesting, runTests, test, waitFor };
|
|
1434
|
+
//# sourceMappingURL=index.mjs.map
|
|
1435
|
+
//# sourceMappingURL=index.mjs.map
|