• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MartinTichovsky / cypress-interceptor / 15779240630

20 Jun 2025 12:44PM UTC coverage: 94.016%. First build
15779240630

Pull #21

github

web-flow
Merge c4f8bc9a6 into bf3c5bc36
Pull Request #21: BREAKING CHANGE: network report

1032 of 1108 branches covered (93.14%)

Branch coverage included in aggregate %.

283 of 344 new or added lines in 9 files covered. (82.27%)

2110 of 2234 relevant lines covered (94.45%)

1367.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.57
/packages/interceptor/src/createXMLHttpRequestProxy.ts
1
import { convertInputBodyToString } from "../convert/convert";
2
import { WindowTypeOfRequestProxy } from "../Interceptor.types";
3
import { lineCalled } from "../test.unit";
4
import { emptyProxy, RequestProxy, RequestProxyFunctionResult } from "./RequestProxy";
5
import { CallLineEnum } from "./test.enum";
6

7
/**
8
 * !! IMPORTANT !!
9
 * There is a bug in the XMLHttpRequest implementation in Cypress. When use a wrapped function like:
10
 *
11
 * @example
12
 * ```ts
13
 *    set onreadystatechange(value: (this: XMLHttpRequest, ev: Event) => unknown) {
14
 *        // this is the wrapped function where the issue is
15
 *        super.onreadystatechange = (ev) => {
16
 *            value.call(this, ev);
17
 *        }
18
 *
19
 *        // this would work without any issue, but we need to use the wrapped function
20
 *        // super.onreadystatechange = value;
21
 *    }
22
 * ```
23
 *
24
 * And use a relative path like:
25
 *
26
 * @example
27
 * ```ts
28
 *       const req = new XMLHttpRequest();
29
 *
30
 *       // when executed on `http://localhost/page.html`
31
 *       req.open("GET", "http://localhost/test", true);
32
 *       req.send();
33
 *       req.onreadystatechange = () => {
34
 *           if (req.readyState === 4) {
35
 *               window.location.href = "./relative-path";
36
 *           }
37
 *       };
38
 * ```
39
 *
40
 * The redirected url will be "./__cypress/iframes/relative-path" instead of "http://localhost/relative-path".
41
 * The issue is when Cypress is resolving the relative path. This `shadow model` of XMLHttpRequest should fix this issue.
42
 */
43
export const createXMLHttpRequestProxy = (
22✔
44
    win: WindowTypeOfRequestProxy,
45
    requestProxy: RequestProxy
46
) => {
47
    if (win.originXMLHttpRequest === undefined) {
1,115✔
48
        win.originXMLHttpRequest = win.XMLHttpRequest;
974✔
49
    }
50

51
    class XMLHttpRequestProxy extends win.originXMLHttpRequest {
52
        private _convertedBody = "";
1,654✔
53
        private _headers: Record<string, string> = {};
1,654✔
54
        private _method: string = "GET";
1,654✔
55
        private _mockBody = false;
1,654✔
56
        private _proxy: RequestProxyFunctionResult = emptyProxy;
1,654✔
57
        private _url: URL = new URL("/", win.location.href);
1,654✔
58
        private shadowXhr: XMLHttpRequest;
59

60
        constructor() {
61
            super();
1,654✔
62

63
            // Create a shadow XMLHttpRequest that won't make real requests
64
            this.shadowXhr = new win.originXMLHttpRequest!();
1,654✔
65

66
            // Initialize default event handlers
67
            this.onabort = () => {
1,654✔
68
                //
69
            };
70

71
            this.onerror = () => {
1,654✔
72
                //
73
            };
74

75
            this.onloadend = () => {
1,654✔
76
                //
77
            };
78

79
            this.onloadstart = () => {
1,654✔
80
                //
81
            };
82

83
            this.onreadystatechange = () => {
1,654✔
84
                //
85
            };
86

87
            this.ontimeout = () => {
1,654✔
88
                //
89
            };
90
        }
91

92
        private createEvent(type: string, originalEvent: Event) {
93
            if (originalEvent instanceof win.ProgressEvent) {
4!
94
                return new win.ProgressEvent(type, {
4✔
95
                    bubbles: originalEvent.bubbles,
96
                    cancelable: originalEvent.cancelable,
97
                    lengthComputable: originalEvent.lengthComputable,
98
                    loaded: originalEvent.loaded,
99
                    total: originalEvent.total
100
                });
101
            }
102

NEW
103
            return new win.Event(type, {
×
104
                bubbles: originalEvent.bubbles,
105
                cancelable: originalEvent.cancelable
106
            });
107
        }
108

109
        private createProgressEvent(type: string, originalEvent: Event) {
110
            return new win.ProgressEvent(type, {
2,942✔
111
                bubbles: originalEvent.bubbles,
112
                cancelable: originalEvent.cancelable
113
            });
114
        }
115

116
        private handleError(error: Error, callLine: CallLineEnum) {
117
            try {
36✔
118
                this._proxy.error(error);
36✔
119
            } catch {
120
                lineCalled(callLine);
6✔
121
            }
122
        }
123

124
        private simulateResponseOnShadow() {
125
            // Set response properties on shadow XMLHttpRequest
126
            Object.defineProperty(this.shadowXhr, "readyState", {
3,248✔
127
                value: this.readyState,
128
                writable: true,
129
                configurable: true
130
            });
131

132
            Object.defineProperty(this.shadowXhr, "status", {
3,248✔
133
                value: this.status,
134
                writable: true,
135
                configurable: true
136
            });
137

138
            Object.defineProperty(this.shadowXhr, "statusText", {
3,248✔
139
                value: this.statusText,
140
                writable: true,
141
                configurable: true
142
            });
143

144
            Object.defineProperty(this.shadowXhr, "responseURL", {
3,248✔
145
                value: this._url.toString(),
146
                writable: true,
147
                configurable: true
148
            });
149

150
            // Handle different response types correctly
151
            const currentResponseType = this.shadowXhr.responseType || "";
3,248✔
152

153
            // Only set responseText if responseType is '' or 'text'
154
            if (currentResponseType === "" || currentResponseType === "text") {
3,248✔
155
                Object.defineProperty(this.shadowXhr, "responseText", {
627✔
156
                    get: () => {
NEW
157
                        const mockBody = this._getMockBody(); // Lazy evaluation - error thrown here if _getMockBody fails
×
NEW
158
                        if (mockBody) {
×
NEW
159
                            return typeof mockBody === "object"
×
160
                                ? JSON.stringify(mockBody)
161
                                : String(mockBody);
162
                        } else {
163
                            // Safely get responseText from super, avoiding our getter
NEW
164
                            try {
×
NEW
165
                                return super.responseText;
×
166
                            } catch {
NEW
167
                                return "";
×
168
                            }
169
                        }
170
                    },
171
                    configurable: true
172
                });
173
            }
174

175
            // Set response property with lazy evaluation
176
            Object.defineProperty(this.shadowXhr, "response", {
3,248✔
177
                get: () => {
NEW
178
                    const mockBody = this._getMockBody(); // Lazy evaluation - error thrown here if _getMockBody fails
×
NEW
179
                    if (mockBody !== undefined) {
×
NEW
180
                        switch (currentResponseType) {
×
181
                            case "json":
NEW
182
                                return typeof mockBody === "object" ? mockBody : mockBody;
×
183
                            case "text":
184
                            case "":
NEW
185
                                return typeof mockBody === "object"
×
186
                                    ? JSON.stringify(mockBody)
187
                                    : String(mockBody);
188
                            case "arraybuffer":
189
                            case "blob":
190
                            case "document":
191
                                // For these types, use the actual response from the main XMLHttpRequest
NEW
192
                                return this.response;
×
193
                            default:
NEW
194
                                return this.response;
×
195
                        }
196
                    } else {
NEW
197
                        return this.response;
×
198
                    }
199
                },
200
                configurable: true
201
            });
202
        }
203

204
        private syncToShadow() {
205
            // Sync timeout
206
            this.shadowXhr.timeout = this.timeout;
1,654✔
207

208
            // Sync withCredentials
209
            this.shadowXhr.withCredentials = this.withCredentials;
1,654✔
210

211
            // Sync responseType
212
            this.shadowXhr.responseType = this.responseType;
1,654✔
213
        }
214

215
        private _getMockBody() {
216
            if (this._proxy.mock && "body" in this._proxy.mock) {
2,988✔
217
                return this._proxy.mock.body;
606✔
218
            } else if (
2,382✔
219
                this._proxy.mock &&
2,786✔
220
                "generateBody" in this._proxy.mock &&
221
                this._proxy.mock.generateBody
222
            ) {
223
                return this._proxy.mock.generateBody(
146✔
224
                    {
225
                        body: this._convertedBody,
226
                        headers: this._headers,
227
                        method: this._method,
228
                        query: Object.fromEntries(this._url.searchParams)
229
                    },
230
                    () => {
231
                        try {
22✔
232
                            return JSON.parse(this._convertedBody);
22✔
233
                        } catch {
234
                            lineCalled(CallLineEnum.n000010);
4✔
235

236
                            return this._convertedBody;
4✔
237
                        }
238
                    }
239
                );
240
            }
241

242
            return undefined;
2,236✔
243
        }
244

245
        private _onResponse = (callback: VoidFunction) => {
1,654✔
246
            try {
3,233✔
247
                this._proxy.done(
3,233✔
248
                    this,
249
                    callback,
250
                    Boolean(
251
                        this._mockBody ||
11,020✔
252
                            this._proxy.mock?.headers ||
253
                            this._proxy.mock?.statusCode ||
254
                            this._proxy.mock?.statusText
255
                    )
256
                );
257
            } catch {
258
                lineCalled(CallLineEnum.n000011);
32✔
259
                callback();
32✔
260
            }
261
        };
262

263
        addEventListener<K extends keyof XMLHttpRequestEventMap>(
264
            type: K,
265
            listener: (this: XMLHttpRequest, ev: XMLHttpRequestEventMap[K]) => unknown,
266
            options?: boolean | AddEventListenerOptions
267
        ) {
268
            let proxyListener = listener;
196✔
269

270
            if (
196✔
271
                type === "load" ||
290✔
272
                type === "loadend" ||
273
                type === "readystatechange" ||
274
                type === "progress"
275
            ) {
276
                proxyListener = (...args) => {
184✔
277
                    const originalEvent = args[0] as Event;
200✔
278

279
                    if (this.readyState === XMLHttpRequest.DONE) {
200✔
280
                        this._onResponse(() => {
176✔
281
                            // Execute on shadow XMLHttpRequest instead of this
282
                            this.simulateResponseOnShadow();
176✔
283

284
                            // Create a new event object to avoid "already being dispatched" error
285
                            const newEvent =
286
                                type === "load" || type === "loadend"
176✔
287
                                    ? this.createProgressEvent(type, originalEvent)
288
                                    : new win.Event("readystatechange", {
289
                                          bubbles: originalEvent.bubbles,
290
                                          cancelable: originalEvent.cancelable
291
                                      });
292

293
                            this.shadowXhr.dispatchEvent(newEvent);
176✔
294
                        });
295
                    } else {
296
                        // For non-DONE states, execute on shadow XMLHttpRequest
297
                        Object.defineProperty(this.shadowXhr, "readyState", {
24✔
298
                            value: this.readyState,
299
                            writable: true,
300
                            configurable: true
301
                        });
302

303
                        // Create a new event object to avoid "already being dispatched" error
304
                        const newEvent =
305
                            type === "load" || type === "loadend"
24!
306
                                ? this.createProgressEvent(type, originalEvent)
307
                                : new win.Event("readystatechange", {
308
                                      bubbles: originalEvent.bubbles,
309
                                      cancelable: originalEvent.cancelable
310
                                  });
311

312
                        this.shadowXhr.dispatchEvent(newEvent);
24✔
313
                    }
314
                };
315
            } else if (type === "loadstart") {
12✔
316
                proxyListener = (...args) => {
2✔
317
                    const originalEvent = args[0] as Event;
2✔
318
                    // loadstart happens at the beginning, so no need to wait for response
319
                    const newEvent = this.createProgressEvent("loadstart", originalEvent);
2✔
320
                    this.shadowXhr.dispatchEvent(newEvent);
2✔
321
                };
322
            } else {
323
                // For other events, execute on shadow XMLHttpRequest
324
                proxyListener = (...args) => {
10✔
325
                    const originalEvent = args[0] as Event;
4✔
326
                    const newEvent = this.createEvent(originalEvent.type, originalEvent);
4✔
327
                    this.shadowXhr.dispatchEvent(newEvent);
4✔
328
                };
329
            }
330

331
            super.addEventListener(type, proxyListener, options);
196✔
332
            this.shadowXhr.addEventListener(type, listener, options);
196✔
333
        }
334

335
        getAllResponseHeaders() {
336
            if (this._proxy.mock?.headers) {
2,886✔
337
                return Object.entries(this._proxy.mock.headers)
672✔
338
                    .map(([key, value]) => `${key}: ${value}`)
672✔
339
                    .join("\r\n");
340
            }
341

342
            return super.getAllResponseHeaders();
2,214✔
343
        }
344

345
        open(...args: [method: string, url: string | URL]) {
346
            this._method = args[0];
1,654✔
347
            this._url = typeof args[1] === "string" ? new URL(args[1], win.location.href) : args[1];
1,654✔
348

349
            // Also open on shadow XMLHttpRequest with same parameters
350
            this.shadowXhr.open(...args);
1,654✔
351

352
            return super.open(...args);
1,654✔
353
        }
354

355
        send(body?: Document | XMLHttpRequestBodyInit | null) {
356
            // Sync properties to shadow before sending
357
            this.syncToShadow();
1,654✔
358

359
            requestProxy
1,654✔
360
                .requestStart(
361
                    {
362
                        body,
363
                        headers: this._headers,
364
                        method: this._method,
365
                        url: this._url
366
                    },
367
                    win,
368
                    "xhr"
369
                )
370
                .then((proxy) => {
371
                    this._mockBody = Boolean(proxy.mock?.body || proxy.mock?.generateBody);
1,650✔
372
                    this._proxy = proxy;
1,650✔
373

374
                    if (!proxy || !this._mockBody) {
1,650✔
375
                        return super.send(body);
1,256✔
376
                    }
377

378
                    convertInputBodyToString(body, win)
394✔
379
                        .then((convertedBody) => {
380
                            this._convertedBody = convertedBody;
390✔
381

382
                            if (proxy.mock?.allowHitTheNetwork) {
390✔
383
                                return super.send(body);
48✔
384
                            } else {
385
                                try {
342✔
386
                                    return proxy.done(
342✔
387
                                        this,
388
                                        () => {
389
                                            this.dispatchEvent(
338✔
390
                                                new win.ProgressEvent("readystatechange")
391
                                            );
392
                                            this.dispatchEvent(new win.ProgressEvent("load"));
338✔
393
                                        },
394
                                        true
395
                                    );
396
                                } catch {
397
                                    lineCalled(CallLineEnum.n000018);
4✔
398

399
                                    return super.send(body);
4✔
400
                                }
401
                            }
402
                        })
403
                        .catch(() => {
404
                            lineCalled(CallLineEnum.n000019);
4✔
405

406
                            try {
4✔
407
                                this._proxy.error(new Error("convertInputBodyToString"));
4✔
408
                            } catch {
409
                                lineCalled(CallLineEnum.n000020);
4✔
410
                            }
411

412
                            return super.send(body);
4✔
413
                        });
414
                })
415
                .catch(() => {
416
                    lineCalled(CallLineEnum.n000021);
4✔
417

418
                    return super.send(body);
4✔
419
                });
420

421
            // DO NOT call this.shadowXhr.send(body) - key requirement
422
        }
423

424
        setRequestHeader(name: string, value: string) {
425
            this._headers[name] = value;
1,986✔
426

427
            // Also set on shadow XMLHttpRequest
428
            this.shadowXhr.setRequestHeader(name, value);
1,986✔
429

430
            super.setRequestHeader(name, value);
1,986✔
431
        }
432

433
        // Getters (alphabetically sorted)
434
        get readyState() {
435
            return this._mockBody && !this._proxy.mock?.allowHitTheNetwork
19,340✔
436
                ? XMLHttpRequest.DONE
437
                : super.readyState;
438
        }
439

440
        get response() {
441
            const mockBody = this._getMockBody();
2,531✔
442

443
            return mockBody
2,527✔
444
                ? typeof mockBody === "object"
678✔
445
                    ? this.responseType === "json"
674✔
446
                        ? mockBody
447
                        : JSON.stringify(mockBody)
448
                    : String(mockBody)
449
                : super.response;
450
        }
451

452
        get responseText() {
453
            // Check if responseType allows access to responseText
454
            const currentResponseType = this.responseType || "";
459✔
455
            if (currentResponseType !== "" && currentResponseType !== "text") {
459✔
456
                throw new win.DOMException(
2✔
457
                    `Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '${currentResponseType}').`,
458
                    "InvalidStateError"
459
                );
460
            }
461

462
            const mockBody = this._getMockBody();
457✔
463

464
            return mockBody
445✔
465
                ? typeof mockBody === "object"
58✔
466
                    ? JSON.stringify(mockBody)
467
                    : String(mockBody)
468
                : super.responseText;
469
        }
470

471
        get status() {
472
            const mock = this._proxy.mock;
6,204✔
473

474
            return mock?.statusCode ?? (mock && !super.status ? 200 : super.status);
6,204✔
475
        }
476

477
        get statusText() {
478
            const mock = this._proxy.mock;
4,764✔
479

480
            return mock?.statusText ?? (mock && !super.statusText ? "OK" : super.statusText);
4,764✔
481
        }
482

483
        // Setters (alphabetically sorted)
484
        set onabort(value: (ev: ProgressEvent<EventTarget>) => unknown) {
485
            super.onabort = (ev) => {
1,670✔
486
                this.handleError(new Error("AbortError"), CallLineEnum.n000016);
22✔
487

488
                // Execute on shadow XMLHttpRequest instead of this
489
                this.shadowXhr.onabort = value;
22✔
490

491
                if (typeof value === "function") {
22!
492
                    this.simulateResponseOnShadow();
22✔
493
                    this.shadowXhr.dispatchEvent(this.createProgressEvent("abort", ev));
22✔
494
                }
495
            };
496
        }
497

498
        set onerror(value: (ev: ProgressEvent<EventTarget>) => unknown) {
499
            super.onerror = (ev) => {
3,187✔
500
                this.handleError(new Error(ev.type), CallLineEnum.n000017);
14✔
501

502
                // Execute on shadow XMLHttpRequest instead of this
503
                this.shadowXhr.onerror = value;
14✔
504

505
                if (typeof value === "function") {
14✔
506
                    this.shadowXhr.dispatchEvent(this.createProgressEvent("error", ev));
12✔
507
                }
508
            };
509
        }
510

511
        set onload(value: (this: XMLHttpRequest, ev: Event) => unknown) {
512
            super.onload = (ev) => {
214✔
513
                this._onResponse(() => {
201✔
514
                    // Execute on shadow XMLHttpRequest instead of this
515
                    this.shadowXhr.onload = value;
201✔
516

517
                    if (typeof value === "function") {
201✔
518
                        this.simulateResponseOnShadow();
198✔
519
                        this.shadowXhr.dispatchEvent(this.createProgressEvent("load", ev));
198✔
520
                    }
521
                });
522
            };
523
        }
524

525
        set onloadend(value: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => unknown) {
526
            super.onloadend = (ev) => {
1,658✔
527
                this._onResponse(() => {
1,251✔
528
                    // Execute on shadow XMLHttpRequest instead of this
529
                    this.shadowXhr.onloadend = value;
1,251✔
530

531
                    if (typeof value === "function") {
1,251!
532
                        this.simulateResponseOnShadow();
1,251✔
533
                        this.shadowXhr.dispatchEvent(this.createProgressEvent("loadend", ev));
1,251✔
534
                    }
535
                });
536
            };
537
        }
538

539
        set onloadstart(value: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => unknown) {
540
            super.onloadstart = (ev) => {
1,664✔
541
                // Execute on shadow XMLHttpRequest instead of this
542
                this.shadowXhr.onloadstart = value;
1,316✔
543

544
                if (typeof value === "function") {
1,316✔
545
                    this.shadowXhr.dispatchEvent(this.createProgressEvent("loadstart", ev));
1,310✔
546
                }
547
            };
548
        }
549

550
        set onprogress(value: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => unknown) {
551
            // For progress events, set the handler directly on the real xhr
552
            // Progress events should flow naturally from the real xhr that's making the request
553
            super.onprogress = value;
10✔
554
        }
555

556
        set onreadystatechange(value: (this: XMLHttpRequest, ev: Event) => unknown) {
557
            super.onreadystatechange = (ev) => {
2,929✔
558
                Object.defineProperty(this.shadowXhr, "readyState", {
5,738✔
559
                    value: this.readyState,
560
                    writable: true,
561
                    configurable: true
562
                });
563

564
                if (this.readyState === XMLHttpRequest.DONE) {
5,738✔
565
                    this._onResponse(() => {
1,605✔
566
                        // Execute on shadow XMLHttpRequest instead of this
567
                        if (typeof value === "function") {
1,605✔
568
                            this.simulateResponseOnShadow();
1,601✔
569
                            this.shadowXhr.onreadystatechange = value;
1,601✔
570
                            this.shadowXhr.dispatchEvent(
1,601✔
571
                                new win.Event("readystatechange", {
572
                                    bubbles: ev.bubbles,
573
                                    cancelable: ev.cancelable
574
                                })
575
                            );
576
                        }
577
                    });
578
                } else {
579
                    // For non-DONE states, still execute on shadow XMLHttpRequest
580
                    if (typeof value === "function") {
4,133✔
581
                        this.shadowXhr.onreadystatechange = value;
4,127✔
582
                        this.shadowXhr.dispatchEvent(
4,127✔
583
                            new win.Event("readystatechange", {
584
                                bubbles: ev.bubbles,
585
                                cancelable: ev.cancelable
586
                            })
587
                        );
588
                    }
589
                }
590
            };
591
        }
592

593
        set ontimeout(value: (this: XMLHttpRequest, ev: ProgressEvent<EventTarget>) => unknown) {
594
            super.ontimeout = (ev) => {
1,660✔
NEW
595
                this.handleError(new Error("TimeoutError"), CallLineEnum.n000022);
×
596

597
                // Execute on shadow XMLHttpRequest instead of this
NEW
598
                this.shadowXhr.ontimeout = value;
×
599

NEW
600
                if (typeof value === "function") {
×
NEW
601
                    this.simulateResponseOnShadow();
×
NEW
602
                    this.shadowXhr.dispatchEvent(this.createProgressEvent("timeout", ev));
×
603
                }
604
            };
605
        }
606
    }
607

608
    win.XMLHttpRequest = XMLHttpRequestProxy;
1,115✔
609
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc