@testspectra/react 1.1.11-rc.5 → 1.1.12
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/cli.js +5 -1
- package/dist/index.js +508 -25
- package/dist/runner.js +1 -1
- package/dist/setup.js +36 -36
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -41,7 +41,11 @@ function buildAutoImportManifest(root) {
|
|
|
41
41
|
function scanFixtures() {
|
|
42
42
|
const dir = path.join(root, "fixtures");
|
|
43
43
|
if (!existsSync(dir)) return [];
|
|
44
|
-
return readdirSync(dir).filter((f) => f.
|
|
44
|
+
return readdirSync(dir).filter((f) => !f.startsWith(".") && !f.endsWith(".d.ts")).map((f) => {
|
|
45
|
+
const ext = path.extname(f);
|
|
46
|
+
const name = path.basename(f, ext);
|
|
47
|
+
return { name, path: path.join(dir, f).replace(/\\/g, "/") };
|
|
48
|
+
});
|
|
45
49
|
}
|
|
46
50
|
return {
|
|
47
51
|
pageObjects: scanEntityDir("page-objects", (p) => `${p}.tsx`),
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,395 @@
|
|
|
1
1
|
// src/dom_driver_bridge.ts
|
|
2
2
|
import { userEvent } from "@testing-library/user-event";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
// ../../core/worker-runtime/src/dsl/mock_intercept.ts
|
|
5
|
+
function matchesUrlPattern(pattern, url) {
|
|
6
|
+
if (!pattern || pattern === "*" || pattern === "**" || pattern === "ALL") {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
10
|
+
try {
|
|
11
|
+
const regex = new RegExp(pattern.slice(1, -1));
|
|
12
|
+
return regex.test(url);
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (pattern.includes("*")) {
|
|
17
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
18
|
+
try {
|
|
19
|
+
const regex = new RegExp(escaped, "i");
|
|
20
|
+
return regex.test(url);
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return url.toLowerCase().includes(pattern.toLowerCase());
|
|
5
25
|
}
|
|
6
|
-
function
|
|
26
|
+
function createMockHandle(rule) {
|
|
27
|
+
const handle = {
|
|
28
|
+
respondWith: async (newFixture, newOptions = {}) => {
|
|
29
|
+
const newObj = newFixture;
|
|
30
|
+
rule.response = newObj && newObj.body !== void 0 ? newObj.body : newFixture;
|
|
31
|
+
if (newOptions.statusCode || newObj && newObj.statusCode) {
|
|
32
|
+
rule.statusCode = newOptions.statusCode || newObj?.statusCode || 200;
|
|
33
|
+
}
|
|
34
|
+
if (newOptions.headers || newObj && newObj.headers) {
|
|
35
|
+
rule.headers = newOptions.headers || newObj?.headers;
|
|
36
|
+
}
|
|
37
|
+
if (newOptions.delayMs !== void 0 || newObj && newObj.delayMs !== void 0) {
|
|
38
|
+
rule.delayMs = newOptions.delayMs ?? newObj?.delayMs;
|
|
39
|
+
}
|
|
40
|
+
rule.aborted = false;
|
|
41
|
+
},
|
|
42
|
+
respondOnce: async (newFixture, newOptions = {}) => {
|
|
43
|
+
const newObj = newFixture;
|
|
44
|
+
const resp = newObj && newObj.body !== void 0 ? newObj.body : newFixture;
|
|
45
|
+
const statusCode = newOptions.statusCode || newObj && newObj.statusCode || rule.statusCode || 200;
|
|
46
|
+
const headers = newOptions.headers || newObj && newObj.headers || rule.headers;
|
|
47
|
+
const delayMs = newOptions.delayMs ?? (newObj && newObj.delayMs) ?? rule.delayMs;
|
|
48
|
+
rule.respondOnceQueue.push({ response: resp, statusCode, headers, delayMs });
|
|
49
|
+
rule.aborted = false;
|
|
50
|
+
},
|
|
51
|
+
abort: async (errorCode = "Failed") => {
|
|
52
|
+
rule.aborted = true;
|
|
53
|
+
rule.abortReason = errorCode;
|
|
54
|
+
},
|
|
55
|
+
waitForCall: async (options = {}) => {
|
|
56
|
+
const targetCount = options.count ?? 1;
|
|
57
|
+
const timeoutMs = options.timeout ?? 5e3;
|
|
58
|
+
if (rule.calls.length >= targetCount) {
|
|
59
|
+
return rule.calls[targetCount - 1];
|
|
60
|
+
}
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const timer = setTimeout(() => {
|
|
63
|
+
const idx = rule.waitResolvers.findIndex((w) => w.resolve === resolve);
|
|
64
|
+
if (idx !== -1) rule.waitResolvers.splice(idx, 1);
|
|
65
|
+
reject(
|
|
66
|
+
new Error(
|
|
67
|
+
`Spectra.intercept waitForCall timed out after ${timeoutMs}ms (expected at least ${targetCount} call(s), received ${rule.calls.length})`
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
rule.waitResolvers.push({
|
|
72
|
+
count: targetCount,
|
|
73
|
+
resolve: (req) => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
resolve(req);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const getCount = () => rule.callCount;
|
|
82
|
+
handle.callCount = Object.assign(getCount, {
|
|
83
|
+
valueOf: () => rule.callCount,
|
|
84
|
+
[Symbol.toPrimitive]: () => rule.callCount
|
|
85
|
+
});
|
|
86
|
+
Object.defineProperty(handle, "calls", {
|
|
87
|
+
get: () => rule.calls,
|
|
88
|
+
enumerable: true
|
|
89
|
+
});
|
|
90
|
+
return handle;
|
|
91
|
+
}
|
|
92
|
+
function parseMockRule(patternOrOptions, maybeMethodOrHandler, maybeFixture, maybeOptions) {
|
|
93
|
+
let pattern;
|
|
94
|
+
let method;
|
|
95
|
+
let response;
|
|
96
|
+
let statusCode;
|
|
97
|
+
let headers;
|
|
98
|
+
let delayMs;
|
|
99
|
+
if (typeof patternOrOptions === "object" && patternOrOptions !== null) {
|
|
100
|
+
pattern = patternOrOptions.url || patternOrOptions.pattern || "*";
|
|
101
|
+
method = (patternOrOptions.method || "GET").toUpperCase();
|
|
102
|
+
response = patternOrOptions.response !== void 0 ? patternOrOptions.response : typeof maybeMethodOrHandler === "object" && maybeMethodOrHandler !== null ? maybeMethodOrHandler : maybeFixture;
|
|
103
|
+
statusCode = patternOrOptions.statusCode || maybeOptions && maybeOptions.statusCode || 200;
|
|
104
|
+
headers = patternOrOptions.headers || maybeOptions && maybeOptions.headers;
|
|
105
|
+
delayMs = patternOrOptions.delayMs ?? (maybeOptions && maybeOptions.delayMs);
|
|
106
|
+
} else {
|
|
107
|
+
pattern = String(patternOrOptions || "*");
|
|
108
|
+
if (typeof maybeMethodOrHandler === "string" && ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"].includes(maybeMethodOrHandler.toUpperCase())) {
|
|
109
|
+
method = maybeMethodOrHandler.toUpperCase();
|
|
110
|
+
const fixtureObj = maybeFixture;
|
|
111
|
+
response = fixtureObj && fixtureObj.body !== void 0 ? fixtureObj.body : maybeFixture;
|
|
112
|
+
statusCode = maybeOptions && maybeOptions.statusCode || fixtureObj && fixtureObj.statusCode || 200;
|
|
113
|
+
headers = maybeOptions && maybeOptions.headers || fixtureObj && fixtureObj.headers;
|
|
114
|
+
delayMs = (maybeOptions && maybeOptions.delayMs) ?? (fixtureObj && fixtureObj.delayMs);
|
|
115
|
+
} else {
|
|
116
|
+
method = "GET";
|
|
117
|
+
const fixtureObj = maybeMethodOrHandler;
|
|
118
|
+
response = fixtureObj && fixtureObj.body !== void 0 ? fixtureObj.body : maybeMethodOrHandler;
|
|
119
|
+
statusCode = maybeFixture && maybeFixture.statusCode || fixtureObj && fixtureObj.statusCode || 200;
|
|
120
|
+
headers = maybeFixture && maybeFixture.headers || fixtureObj && fixtureObj.headers;
|
|
121
|
+
delayMs = (maybeFixture && maybeFixture.delayMs) ?? (fixtureObj && fixtureObj.delayMs);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
pattern,
|
|
126
|
+
method,
|
|
127
|
+
response,
|
|
128
|
+
statusCode,
|
|
129
|
+
headers,
|
|
130
|
+
delayMs,
|
|
131
|
+
callCount: 0,
|
|
132
|
+
respondOnceQueue: [],
|
|
133
|
+
calls: [],
|
|
134
|
+
waitResolvers: []
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function findMatchingRule(rules, url, method) {
|
|
138
|
+
for (const rule of rules) {
|
|
139
|
+
if (matchesUrlPattern(rule.pattern, url)) {
|
|
140
|
+
if (!rule.method || rule.method === "ALL" || rule.method.toUpperCase() === method.toUpperCase()) {
|
|
141
|
+
return rule;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
function recordMockCall(rule, request) {
|
|
148
|
+
rule.calls.push(request);
|
|
149
|
+
rule.callCount++;
|
|
150
|
+
for (let i = rule.waitResolvers.length - 1; i >= 0; i--) {
|
|
151
|
+
const w = rule.waitResolvers[i];
|
|
152
|
+
if (rule.callCount >= w.count) {
|
|
153
|
+
rule.waitResolvers.splice(i, 1);
|
|
154
|
+
w.resolve(request);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return request;
|
|
158
|
+
}
|
|
159
|
+
function consumeMockResponse(rule) {
|
|
160
|
+
if (rule.respondOnceQueue.length > 0) {
|
|
161
|
+
const queued = rule.respondOnceQueue.shift();
|
|
162
|
+
return {
|
|
163
|
+
body: queued.response,
|
|
164
|
+
statusCode: queued.statusCode || rule.statusCode || 200,
|
|
165
|
+
headers: { ...rule.headers, ...queued.headers },
|
|
166
|
+
delayMs: queued.delayMs ?? rule.delayMs
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
body: rule.response,
|
|
171
|
+
statusCode: rule.statusCode || 200,
|
|
172
|
+
headers: { ...rule.headers },
|
|
173
|
+
delayMs: rule.delayMs
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
var DEFAULT_MOCK_RESPONSE_HEADERS = {
|
|
177
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
178
|
+
"Access-Control-Allow-Origin": "*",
|
|
179
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
180
|
+
"Access-Control-Allow-Headers": "*"
|
|
181
|
+
};
|
|
182
|
+
function mergeMockResponseHeaders(custom) {
|
|
183
|
+
const merged = /* @__PURE__ */ new Map();
|
|
184
|
+
for (const [k, v] of Object.entries(DEFAULT_MOCK_RESPONSE_HEADERS)) {
|
|
185
|
+
merged.set(k.toLowerCase(), { name: k, value: v });
|
|
186
|
+
}
|
|
187
|
+
for (const [k, v] of Object.entries(custom || {})) {
|
|
188
|
+
merged.set(k.toLowerCase(), { name: k, value: String(v) });
|
|
189
|
+
}
|
|
190
|
+
return [...merged.values()];
|
|
191
|
+
}
|
|
192
|
+
function mockResponseBodyText(body) {
|
|
193
|
+
if (typeof body === "string") return body;
|
|
194
|
+
if (typeof body === "object" && body !== null) return JSON.stringify(body);
|
|
195
|
+
return String(body ?? "");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ../../core/worker-runtime/src/dsl/selector_resolver.ts
|
|
199
|
+
function parseA11ySelector(selector) {
|
|
7
200
|
const trimmed = selector.trim();
|
|
8
|
-
if (trimmed.startsWith("~"))
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
201
|
+
if (!trimmed.startsWith("~")) return null;
|
|
202
|
+
const withoutPrefix = trimmed.slice(1);
|
|
203
|
+
const bracketIndex = withoutPrefix.indexOf("[");
|
|
204
|
+
if (bracketIndex !== -1) {
|
|
205
|
+
return {
|
|
206
|
+
name: withoutPrefix.slice(0, bracketIndex),
|
|
207
|
+
chainSuffix: withoutPrefix.slice(bracketIndex)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return { name: withoutPrefix, chainSuffix: "" };
|
|
211
|
+
}
|
|
212
|
+
function toCssSelectorA11y(selector) {
|
|
213
|
+
const parsed = parseA11ySelector(selector);
|
|
214
|
+
if (!parsed) return null;
|
|
215
|
+
const { name, chainSuffix } = parsed;
|
|
216
|
+
const q = JSON.stringify(name);
|
|
217
|
+
return `:is([aria-label=${q}], [title=${q}])${chainSuffix}`;
|
|
218
|
+
}
|
|
219
|
+
function resolveLabelledBy(name, root, index) {
|
|
220
|
+
const all = Array.from(root.querySelectorAll("[aria-labelledby]"));
|
|
221
|
+
const matches = all.filter((el) => {
|
|
222
|
+
const ids = (el.getAttribute("aria-labelledby") ?? "").trim().split(/\s+/);
|
|
223
|
+
const label = ids.map((id) => {
|
|
224
|
+
const ref = document.getElementById(id);
|
|
225
|
+
return ref ? (ref.textContent ?? "").trim() : "";
|
|
226
|
+
}).filter(Boolean).join(" ").trim();
|
|
227
|
+
return label === name;
|
|
228
|
+
});
|
|
229
|
+
if (index === null) return matches[0] ?? null;
|
|
230
|
+
if (index >= 0) return matches[index] ?? null;
|
|
231
|
+
return matches[matches.length + index] ?? null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/network_patch.ts
|
|
235
|
+
var fetchPatched = false;
|
|
236
|
+
var xhrPatched = false;
|
|
237
|
+
var activeGetRules = null;
|
|
238
|
+
function sleep(ms) {
|
|
239
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
240
|
+
}
|
|
241
|
+
function toAbsoluteUrl(input) {
|
|
242
|
+
try {
|
|
243
|
+
if (typeof input === "string") return new URL(input, selfURL()).href;
|
|
244
|
+
if (input instanceof URL) return input.href;
|
|
245
|
+
if (typeof Request !== "undefined" && input instanceof Request) return input.url;
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
return String(input);
|
|
249
|
+
}
|
|
250
|
+
function selfURL() {
|
|
251
|
+
try {
|
|
252
|
+
if (typeof location !== "undefined" && location.href) return location.href;
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
return "http://localhost/";
|
|
256
|
+
}
|
|
257
|
+
function readMethod(input, init) {
|
|
258
|
+
if (init?.method) return String(init.method).toUpperCase();
|
|
259
|
+
try {
|
|
260
|
+
if (typeof Request !== "undefined" && input instanceof Request && input.method) {
|
|
261
|
+
return String(input.method).toUpperCase();
|
|
15
262
|
}
|
|
16
|
-
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
return "GET";
|
|
266
|
+
}
|
|
267
|
+
function readPostData(input, init) {
|
|
268
|
+
const body = init?.body;
|
|
269
|
+
if (typeof body === "string") return body;
|
|
270
|
+
try {
|
|
271
|
+
if (typeof Request !== "undefined" && input instanceof Request) return void 0;
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
return void 0;
|
|
275
|
+
}
|
|
276
|
+
function buildMockResponse(consumed) {
|
|
277
|
+
const headerPairs = mergeMockResponseHeaders(consumed.headers);
|
|
278
|
+
const headers = {};
|
|
279
|
+
for (const h of headerPairs) headers[h.name] = h.value;
|
|
280
|
+
return new Response(mockResponseBodyText(consumed.body), { status: consumed.statusCode, headers });
|
|
281
|
+
}
|
|
282
|
+
async function patchedFetch(originalFetch, input, init) {
|
|
283
|
+
const url = toAbsoluteUrl(input);
|
|
284
|
+
const method = readMethod(input, init);
|
|
285
|
+
const rule = findMatchingRule(activeGetRules?.() ?? [], url, method);
|
|
286
|
+
if (!rule) return originalFetch(input, init);
|
|
287
|
+
const intercepted = {
|
|
288
|
+
url,
|
|
289
|
+
method,
|
|
290
|
+
headers: void 0,
|
|
291
|
+
postData: readPostData(input, init),
|
|
292
|
+
timestamp: Date.now()
|
|
293
|
+
};
|
|
294
|
+
recordMockCall(rule, intercepted);
|
|
295
|
+
if (rule.aborted) {
|
|
296
|
+
throw new TypeError(`Spectra.intercept() aborted ${method} ${url} (${rule.abortReason || "Failed"})`);
|
|
297
|
+
}
|
|
298
|
+
const consumed = consumeMockResponse(rule);
|
|
299
|
+
if (consumed.delayMs && consumed.delayMs > 0) await sleep(consumed.delayMs);
|
|
300
|
+
return buildMockResponse(consumed);
|
|
301
|
+
}
|
|
302
|
+
function patchXMLHttpRequest() {
|
|
303
|
+
const OriginalXHR = globalThis.XMLHttpRequest;
|
|
304
|
+
function PatchedXHR() {
|
|
305
|
+
const xhr = new OriginalXHR();
|
|
306
|
+
let method = "GET";
|
|
307
|
+
let url = "";
|
|
308
|
+
const origOpen = OriginalXHR.prototype.open.bind(xhr);
|
|
309
|
+
const origSend = OriginalXHR.prototype.send.bind(xhr);
|
|
310
|
+
xhr.open = (m, u, ...rest) => {
|
|
311
|
+
method = String(m).toUpperCase();
|
|
312
|
+
url = toAbsoluteUrl(u);
|
|
313
|
+
const [asyncFlag = true, username, password] = rest;
|
|
314
|
+
return origOpen(m, u, asyncFlag, username, password);
|
|
315
|
+
};
|
|
316
|
+
xhr.send = (body) => {
|
|
317
|
+
const rule = findMatchingRule(activeGetRules?.() ?? [], url, method);
|
|
318
|
+
if (!rule) return origSend(body);
|
|
319
|
+
const intercepted = {
|
|
320
|
+
url,
|
|
321
|
+
method,
|
|
322
|
+
headers: void 0,
|
|
323
|
+
postData: typeof body === "string" ? body : void 0,
|
|
324
|
+
timestamp: Date.now()
|
|
325
|
+
};
|
|
326
|
+
recordMockCall(rule, intercepted);
|
|
327
|
+
const fireError = () => {
|
|
328
|
+
xhr.dispatchEvent(new Event("error"));
|
|
329
|
+
};
|
|
330
|
+
if (rule.aborted) {
|
|
331
|
+
setTimeout(fireError, 0);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const consumed = consumeMockResponse(rule);
|
|
335
|
+
const respond2 = () => {
|
|
336
|
+
const bodyText = mockResponseBodyText(consumed.body);
|
|
337
|
+
Object.defineProperties(xhr, {
|
|
338
|
+
status: { value: consumed.statusCode, configurable: true },
|
|
339
|
+
statusText: {
|
|
340
|
+
value: consumed.statusCode === 200 ? "OK" : `${consumed.statusCode} Mocked`,
|
|
341
|
+
configurable: true
|
|
342
|
+
},
|
|
343
|
+
responseText: { value: bodyText, configurable: true },
|
|
344
|
+
response: { value: bodyText, configurable: true },
|
|
345
|
+
readyState: { value: 4, configurable: true }
|
|
346
|
+
});
|
|
347
|
+
xhr.dispatchEvent(new Event("readystatechange"));
|
|
348
|
+
xhr.dispatchEvent(new Event("load"));
|
|
349
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
350
|
+
};
|
|
351
|
+
if (consumed.delayMs && consumed.delayMs > 0) setTimeout(respond2, consumed.delayMs);
|
|
352
|
+
else setTimeout(respond2, 0);
|
|
353
|
+
};
|
|
354
|
+
return xhr;
|
|
355
|
+
}
|
|
356
|
+
PatchedXHR.prototype = OriginalXHR.prototype;
|
|
357
|
+
globalThis.XMLHttpRequest = PatchedXHR;
|
|
358
|
+
}
|
|
359
|
+
function ensureNetworkPatch(getRules) {
|
|
360
|
+
activeGetRules = getRules;
|
|
361
|
+
if (!fetchPatched && typeof globalThis.fetch === "function") {
|
|
362
|
+
const originalFetch = globalThis.fetch.bind(globalThis);
|
|
363
|
+
globalThis.fetch = (input, init) => patchedFetch(originalFetch, input, init);
|
|
364
|
+
fetchPatched = true;
|
|
365
|
+
}
|
|
366
|
+
if (!xhrPatched && typeof globalThis.XMLHttpRequest === "function") {
|
|
367
|
+
patchXMLHttpRequest();
|
|
368
|
+
xhrPatched = true;
|
|
17
369
|
}
|
|
18
|
-
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/dom_driver_bridge.ts
|
|
373
|
+
function getUser() {
|
|
374
|
+
return userEvent.setup({ delay: null });
|
|
375
|
+
}
|
|
376
|
+
function resolveSelector(selector) {
|
|
377
|
+
const css = toCssSelectorA11y(selector.trim());
|
|
378
|
+
return css ?? selector.trim();
|
|
19
379
|
}
|
|
20
380
|
function resolveEl(selector, index) {
|
|
21
381
|
const css = resolveSelector(selector);
|
|
22
|
-
|
|
23
|
-
return document.querySelector(css);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
382
|
+
const fromCss = () => {
|
|
383
|
+
if (index === null) return document.querySelector(css);
|
|
384
|
+
const all = Array.from(document.querySelectorAll(css));
|
|
385
|
+
if (index >= 0) return all[index] ?? null;
|
|
386
|
+
return all[all.length + index] ?? null;
|
|
387
|
+
};
|
|
388
|
+
const el = fromCss();
|
|
389
|
+
if (el !== null) return el;
|
|
390
|
+
const parsed = parseA11ySelector(selector.trim());
|
|
391
|
+
if (parsed) return resolveLabelledBy(parsed.name, document, index);
|
|
392
|
+
return null;
|
|
28
393
|
}
|
|
29
394
|
function isFormEl(el) {
|
|
30
395
|
const tag = el.tagName;
|
|
@@ -98,12 +463,98 @@ var DomDriverBridge = class {
|
|
|
98
463
|
async setValue(selector, value, index = null) {
|
|
99
464
|
const el = await this.waitForEl(selector, index);
|
|
100
465
|
if (!isFormEl(el)) throw new Error("Element is not a form field: " + JSON.stringify(selector));
|
|
466
|
+
if (el.tagName === "INPUT" && el.type === "file") {
|
|
467
|
+
return this.upload(selector, value, index);
|
|
468
|
+
}
|
|
101
469
|
const u = getUser();
|
|
102
470
|
await u.clear(el);
|
|
103
471
|
if (String(value).length > 0) {
|
|
104
472
|
await u.type(el, String(value));
|
|
105
473
|
}
|
|
106
474
|
}
|
|
475
|
+
async upload(selector, files, index = null) {
|
|
476
|
+
const el = await this.waitForEl(selector, index);
|
|
477
|
+
const fileList = Array.isArray(files) ? files : [files];
|
|
478
|
+
const filesToUpload = [];
|
|
479
|
+
const MIME_MAP = {
|
|
480
|
+
png: "image/png",
|
|
481
|
+
jpg: "image/jpeg",
|
|
482
|
+
jpeg: "image/jpeg",
|
|
483
|
+
gif: "image/gif",
|
|
484
|
+
svg: "image/svg+xml",
|
|
485
|
+
webp: "image/webp",
|
|
486
|
+
pdf: "application/pdf",
|
|
487
|
+
json: "application/json",
|
|
488
|
+
txt: "text/plain",
|
|
489
|
+
csv: "text/csv"
|
|
490
|
+
};
|
|
491
|
+
for (const item of fileList) {
|
|
492
|
+
if (typeof item === "string") {
|
|
493
|
+
let fileName = "file.bin";
|
|
494
|
+
let mimeType = "application/octet-stream";
|
|
495
|
+
let blob;
|
|
496
|
+
if (item.startsWith("data:")) {
|
|
497
|
+
const commaIdx = item.indexOf(",");
|
|
498
|
+
const header = item.slice(0, commaIdx);
|
|
499
|
+
const base64Data = item.slice(commaIdx + 1);
|
|
500
|
+
const mimeMatch = header.match(/data:([^;]+)/);
|
|
501
|
+
if (mimeMatch) mimeType = mimeMatch[1];
|
|
502
|
+
const nameMatch = header.match(/(?:name|filename)=([^;]+)/i);
|
|
503
|
+
if (nameMatch) {
|
|
504
|
+
fileName = decodeURIComponent(nameMatch[1]);
|
|
505
|
+
} else {
|
|
506
|
+
const ext = mimeType.split("/")[1] || "bin";
|
|
507
|
+
fileName = `fixture.${ext}`;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const binaryStr = atob(base64Data);
|
|
511
|
+
const bytes = new Uint8Array(binaryStr.length);
|
|
512
|
+
for (let i = 0; i < binaryStr.length; i++) {
|
|
513
|
+
bytes[i] = binaryStr.charCodeAt(i);
|
|
514
|
+
}
|
|
515
|
+
blob = new Blob([bytes], { type: mimeType });
|
|
516
|
+
} catch {
|
|
517
|
+
blob = new Blob([base64Data], { type: mimeType });
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
const cleanPath = item.split("?")[0].split("#")[0];
|
|
521
|
+
fileName = cleanPath.split("/").pop()?.split("\\").pop() || "fixture.bin";
|
|
522
|
+
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
523
|
+
mimeType = MIME_MAP[ext] || "application/octet-stream";
|
|
524
|
+
blob = new Blob(["Mock file content for " + fileName], { type: mimeType });
|
|
525
|
+
}
|
|
526
|
+
filesToUpload.push(new File([blob], fileName, { type: mimeType }));
|
|
527
|
+
} else if (item instanceof File) {
|
|
528
|
+
filesToUpload.push(item);
|
|
529
|
+
} else if (item && typeof item === "object" && "name" in item) {
|
|
530
|
+
const obj = item;
|
|
531
|
+
const mimeType = obj.type || "application/octet-stream";
|
|
532
|
+
let blob;
|
|
533
|
+
if (obj.base64) {
|
|
534
|
+
try {
|
|
535
|
+
const binaryStr = atob(obj.base64);
|
|
536
|
+
const bytes = new Uint8Array(binaryStr.length);
|
|
537
|
+
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
|
|
538
|
+
blob = new Blob([bytes], { type: mimeType });
|
|
539
|
+
} catch {
|
|
540
|
+
blob = new Blob([obj.base64], { type: mimeType });
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
blob = new Blob([obj.content || "Mock content"], { type: mimeType });
|
|
544
|
+
}
|
|
545
|
+
filesToUpload.push(new File([blob], obj.name, { type: mimeType }));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (filesToUpload.length > 0) {
|
|
549
|
+
const dt = new DataTransfer();
|
|
550
|
+
for (const f of filesToUpload) {
|
|
551
|
+
dt.items.add(f);
|
|
552
|
+
}
|
|
553
|
+
el.files = dt.files;
|
|
554
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
555
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
107
558
|
async clearValue(selector, index = null) {
|
|
108
559
|
const el = await this.waitForEl(selector, index);
|
|
109
560
|
await getUser().clear(el);
|
|
@@ -311,13 +762,15 @@ var DomDriverBridge = class {
|
|
|
311
762
|
async clearLocalStorage() {
|
|
312
763
|
window.localStorage.clear();
|
|
313
764
|
}
|
|
314
|
-
// ─── Network Interception (
|
|
315
|
-
async intercept(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
);
|
|
765
|
+
// ─── Network Interception (in-page fetch/XHR patch — see network_patch.ts) ──
|
|
766
|
+
async intercept(patternOrOptions, method, fixture, options) {
|
|
767
|
+
const rule = parseMockRule(patternOrOptions, method, fixture, options);
|
|
768
|
+
ensureNetworkPatch(() => this.mockRules);
|
|
769
|
+
this.mockRules.push(rule);
|
|
770
|
+
return createMockHandle(rule);
|
|
319
771
|
}
|
|
320
772
|
clearMocks() {
|
|
773
|
+
this.mockRules = [];
|
|
321
774
|
}
|
|
322
775
|
};
|
|
323
776
|
|
|
@@ -413,7 +866,11 @@ function buildAutoImportManifest(root) {
|
|
|
413
866
|
function scanFixtures() {
|
|
414
867
|
const dir = path.join(root, "fixtures");
|
|
415
868
|
if (!existsSync(dir)) return [];
|
|
416
|
-
return readdirSync(dir).filter((f) => f.
|
|
869
|
+
return readdirSync(dir).filter((f) => !f.startsWith(".") && !f.endsWith(".d.ts")).map((f) => {
|
|
870
|
+
const ext = path.extname(f);
|
|
871
|
+
const name = path.basename(f, ext);
|
|
872
|
+
return { name, path: path.join(dir, f).replace(/\\/g, "/") };
|
|
873
|
+
});
|
|
417
874
|
}
|
|
418
875
|
return {
|
|
419
876
|
pageObjects: scanEntityDir("page-objects", (p) => `${p}.tsx`),
|
|
@@ -712,7 +1169,10 @@ async function buildBundle(entrypoint, opts = {}) {
|
|
|
712
1169
|
".woff": "dataurl",
|
|
713
1170
|
".woff2": "dataurl",
|
|
714
1171
|
".ttf": "dataurl",
|
|
715
|
-
".eot": "dataurl"
|
|
1172
|
+
".eot": "dataurl",
|
|
1173
|
+
".pdf": "dataurl",
|
|
1174
|
+
".txt": "text",
|
|
1175
|
+
".csv": "text"
|
|
716
1176
|
},
|
|
717
1177
|
logLevel: "error",
|
|
718
1178
|
logOverride: {
|
|
@@ -981,6 +1441,29 @@ function makeHandler(testRoot, packageDir, harnessHtml, setupEntrypoint, runnerE
|
|
|
981
1441
|
}
|
|
982
1442
|
return;
|
|
983
1443
|
}
|
|
1444
|
+
const lower = resolved.toLowerCase();
|
|
1445
|
+
if (lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg") || lower.endsWith(".gif") || lower.endsWith(".webp") || lower.endsWith(".svg") || lower.endsWith(".pdf")) {
|
|
1446
|
+
try {
|
|
1447
|
+
const raw = fs.readFileSync(resolved);
|
|
1448
|
+
const ext = path3.extname(lower);
|
|
1449
|
+
const fileName = path3.basename(resolved);
|
|
1450
|
+
const mime = ext === ".pdf" ? "application/pdf" : ext === ".svg" ? "image/svg+xml" : `image/${ext.replace(/^\./, "")}`;
|
|
1451
|
+
const dataUrl = `data:${mime};name=${encodeURIComponent(fileName)};base64,${raw.toString("base64")}`;
|
|
1452
|
+
respond(res, 200, "application/javascript; charset=utf-8", `export default ${JSON.stringify(dataUrl)};`);
|
|
1453
|
+
} catch {
|
|
1454
|
+
respond(res, 500, "application/javascript", `export default ${JSON.stringify(resolved)};`);
|
|
1455
|
+
}
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (lower.endsWith(".txt") || lower.endsWith(".csv")) {
|
|
1459
|
+
try {
|
|
1460
|
+
const raw = fs.readFileSync(resolved, "utf-8");
|
|
1461
|
+
respond(res, 200, "application/javascript; charset=utf-8", `export default ${JSON.stringify(raw)};`);
|
|
1462
|
+
} catch {
|
|
1463
|
+
respond(res, 500, "application/javascript", `export default ${JSON.stringify(resolved)};`);
|
|
1464
|
+
}
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
984
1467
|
try {
|
|
985
1468
|
const js = await getCachedBundle(resolved, resolved);
|
|
986
1469
|
respond(res, 200, "application/javascript; charset=utf-8", js);
|
package/dist/runner.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var T=[],c=[],f=[];globalThis.it=function(n,o){T.push({title:n,fn:o})};globalThis.beforeEach=function(n){c.push(n)};globalThis.afterEach=function(n){f.push(n)};function R(){let n=globalThis.Spectra;if(typeof n?.unmount=="function")n.unmount();else{let o=document.getElementById("spectra-root");o&&(o.innerHTML="")}}function e(n,o){console.log(`[${n}] ${o}`)}globalThis.__SPECTRA_RUNNER__={async run(n,o){e("TESTSPECTRA_SUITE_START",JSON.stringify({file:n}));let u=globalThis.__bootstrapAutoImports;if(typeof u=="function")try{await u(o)}catch(t){return e("TESTSPECTRA_SUITE_ERROR",JSON.stringify({file:n,error:`Failed to bootstrap auto imports: ${String(t)}`})),{passed:0,failed:1}}let
|
|
1
|
+
var T=[],c=[],f=[];globalThis.it=function(n,o){T.push({title:n,fn:o})};globalThis.beforeEach=function(n){c.push(n)};globalThis.afterEach=function(n){f.push(n)};function R(){let n=globalThis.Spectra;if(typeof n?.unmount=="function")n.unmount();else{let o=document.getElementById("spectra-root");o&&(o.innerHTML="")}}function e(n,o){console.log(`[${n}] ${o}`)}globalThis.__SPECTRA_RUNNER__={async run(n,o){e("TESTSPECTRA_SUITE_START",JSON.stringify({file:n}));try{globalThis.__TS_RUNTIME__.activeDriver?.clearMocks?.()}catch{}let u=globalThis.__bootstrapAutoImports;if(typeof u=="function")try{await u(o)}catch(t){return e("TESTSPECTRA_SUITE_ERROR",JSON.stringify({file:n,error:`Failed to bootstrap auto imports: ${String(t)}`})),{passed:0,failed:1}}let E=encodeURIComponent(n);try{await import(`/test?file=${E}`)}catch(t){return e("TESTSPECTRA_SUITE_ERROR",JSON.stringify({file:n,error:String(t)})),{passed:0,failed:1}}let i=0,a=0;for(let{title:t,fn:_}of T){e("TESTSPECTRA_TEST_START",t),globalThis.__CURRENT_TEST_TITLE__=t;let S={task:{name:t}},l=performance.now();try{for(let s of c)await s(S);await _();for(let s of f)await s(S);let r=Math.round(performance.now()-l);e("TESTSPECTRA_TEST_PASS",`${t} (${r}ms)`),i++}catch(r){let s=Math.round(performance.now()-l),g=r instanceof Error?r.message:String(r);e("TESTSPECTRA_TEST_ERROR",g),e("TESTSPECTRA_TEST_FAIL",`${t} (${s}ms)`),a++}finally{R()}}return T.length=0,c.length=0,f.length=0,e("TESTSPECTRA_SUITE_END",JSON.stringify({file:n,passed:i,failed:a})),{passed:i,failed:a}}};
|