@testspectra/react 1.1.11-rc.5 → 1.1.13

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 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.endsWith(".json") && !f.startsWith(".")).map((f) => ({ name: f.replace(/\.json$/, ""), path: path.join(dir, f).replace(/\\/g, "/") }));
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
- function getUser() {
4
- return userEvent.setup({ delay: null });
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 resolveSelector(selector) {
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
- const raw = trimmed.slice(1);
10
- const bracketIndex = raw.indexOf("[");
11
- if (bracketIndex !== -1) {
12
- const label = raw.slice(0, bracketIndex);
13
- const rest = raw.slice(bracketIndex);
14
- return `[aria-label=${JSON.stringify(label)}]${rest}`;
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
- return `[aria-label=${JSON.stringify(raw)}]`;
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
- return trimmed;
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
- if (index === null) {
23
- return document.querySelector(css);
24
- }
25
- const all = Array.from(document.querySelectorAll(css));
26
- if (index >= 0) return all[index] ?? null;
27
- return all[all.length + index] ?? null;
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 (Out of Scope for Component Testing MVP) ─────
315
- async intercept(_patternOrOptions, _method, _fixture, _options) {
316
- throw new Error(
317
- 'Spectra.intercept() is not supported in component testing mode \u2014 there is no network boundary between the test and the mounted component to intercept. Mock fetch/HTTP clients directly instead (e.g. `vi.spyOn(global, "fetch")` or MSW).'
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.endsWith(".json") && !f.startsWith(".")).map((f) => ({ name: f.replace(/\.json$/, ""), path: path.join(dir, f).replace(/\\/g, "/") }));
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 l=encodeURIComponent(n);try{await import(`/test?file=${l}`)}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:d}of T){e("TESTSPECTRA_TEST_START",t),globalThis.__CURRENT_TEST_TITLE__=t;let S={task:{name:t}},E=performance.now();try{for(let s of c)await s(S);await d();for(let s of f)await s(S);let r=Math.round(performance.now()-E);e("TESTSPECTRA_TEST_PASS",`${t} (${r}ms)`),i++}catch(r){let s=Math.round(performance.now()-E),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}}};
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}}};