1/**
2 * Copyright 2018 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import fs from 'fs';
18import path from 'path';
19import utils from './utils.js';
20import expect from 'expect';
21import {
22  getTestState,
23  setupTestBrowserHooks,
24  setupTestPageAndContextHooks,
25  describeFailsFirefox,
26} from './mocha-utils'; // eslint-disable-line import/extensions
27
28describe('request interception', function () {
29  setupTestBrowserHooks();
30  setupTestPageAndContextHooks();
31  describe('Page.setRequestInterception', function () {
32    it('should intercept', async () => {
33      const { page, server } = getTestState();
34
35      await page.setRequestInterception(true);
36      page.on('request', (request) => {
37        if (utils.isFavicon(request)) {
38          request.continue();
39          return;
40        }
41        expect(request.url()).toContain('empty.html');
42        expect(request.headers()['user-agent']).toBeTruthy();
43        expect(request.method()).toBe('GET');
44        expect(request.postData()).toBe(undefined);
45        expect(request.isNavigationRequest()).toBe(true);
46        expect(request.resourceType()).toBe('document');
47        expect(request.frame() === page.mainFrame()).toBe(true);
48        expect(request.frame().url()).toBe('about:blank');
49        request.continue();
50      });
51      const response = await page.goto(server.EMPTY_PAGE);
52      expect(response.ok()).toBe(true);
53      expect(response.remoteAddress().port).toBe(server.PORT);
54    });
55    it('should work when POST is redirected with 302', async () => {
56      const { page, server } = getTestState();
57
58      server.setRedirect('/rredirect', '/empty.html');
59      await page.goto(server.EMPTY_PAGE);
60      await page.setRequestInterception(true);
61      page.on('request', (request) => request.continue());
62      await page.setContent(`
63        <form action='/rredirect' method='post'>
64          <input type="hidden" id="foo" name="foo" value="FOOBAR">
65        </form>
66      `);
67      await Promise.all([
68        page.$eval('form', (form: HTMLFormElement) => form.submit()),
69        page.waitForNavigation(),
70      ]);
71    });
72    // @see https://github.com/puppeteer/puppeteer/issues/3973
73    it('should work when header manipulation headers with redirect', async () => {
74      const { page, server } = getTestState();
75
76      server.setRedirect('/rrredirect', '/empty.html');
77      await page.setRequestInterception(true);
78      page.on('request', (request) => {
79        const headers = Object.assign({}, request.headers(), {
80          foo: 'bar',
81        });
82        request.continue({ headers });
83      });
84      await page.goto(server.PREFIX + '/rrredirect');
85    });
86    // @see https://github.com/puppeteer/puppeteer/issues/4743
87    it('should be able to remove headers', async () => {
88      const { page, server } = getTestState();
89
90      await page.setRequestInterception(true);
91      page.on('request', (request) => {
92        const headers = Object.assign({}, request.headers(), {
93          foo: 'bar',
94          origin: undefined, // remove "origin" header
95        });
96        request.continue({ headers });
97      });
98
99      const [serverRequest] = await Promise.all([
100        server.waitForRequest('/empty.html'),
101        page.goto(server.PREFIX + '/empty.html'),
102      ]);
103
104      expect(serverRequest.headers.origin).toBe(undefined);
105    });
106    it('should contain referer header', async () => {
107      const { page, server } = getTestState();
108
109      await page.setRequestInterception(true);
110      const requests = [];
111      page.on('request', (request) => {
112        if (!utils.isFavicon(request)) requests.push(request);
113        request.continue();
114      });
115      await page.goto(server.PREFIX + '/one-style.html');
116      expect(requests[1].url()).toContain('/one-style.css');
117      expect(requests[1].headers().referer).toContain('/one-style.html');
118    });
119    it('should properly return navigation response when URL has cookies', async () => {
120      const { page, server } = getTestState();
121
122      // Setup cookie.
123      await page.goto(server.EMPTY_PAGE);
124      await page.setCookie({ name: 'foo', value: 'bar' });
125
126      // Setup request interception.
127      await page.setRequestInterception(true);
128      page.on('request', (request) => request.continue());
129      const response = await page.reload();
130      expect(response.status()).toBe(200);
131    });
132    it('should stop intercepting', async () => {
133      const { page, server } = getTestState();
134
135      await page.setRequestInterception(true);
136      page.once('request', (request) => request.continue());
137      await page.goto(server.EMPTY_PAGE);
138      await page.setRequestInterception(false);
139      await page.goto(server.EMPTY_PAGE);
140    });
141    it('should show custom HTTP headers', async () => {
142      const { page, server } = getTestState();
143
144      await page.setExtraHTTPHeaders({
145        foo: 'bar',
146      });
147      await page.setRequestInterception(true);
148      page.on('request', (request) => {
149        expect(request.headers()['foo']).toBe('bar');
150        request.continue();
151      });
152      const response = await page.goto(server.EMPTY_PAGE);
153      expect(response.ok()).toBe(true);
154    });
155    // @see https://github.com/puppeteer/puppeteer/issues/4337
156    it('should work with redirect inside sync XHR', async () => {
157      const { page, server } = getTestState();
158
159      await page.goto(server.EMPTY_PAGE);
160      server.setRedirect('/logo.png', '/pptr.png');
161      await page.setRequestInterception(true);
162      page.on('request', (request) => request.continue());
163      const status = await page.evaluate(async () => {
164        const request = new XMLHttpRequest();
165        request.open('GET', '/logo.png', false); // `false` makes the request synchronous
166        request.send(null);
167        return request.status;
168      });
169      expect(status).toBe(200);
170    });
171    it('should work with custom referer headers', async () => {
172      const { page, server } = getTestState();
173
174      await page.setExtraHTTPHeaders({ referer: server.EMPTY_PAGE });
175      await page.setRequestInterception(true);
176      page.on('request', (request) => {
177        expect(request.headers()['referer']).toBe(server.EMPTY_PAGE);
178        request.continue();
179      });
180      const response = await page.goto(server.EMPTY_PAGE);
181      expect(response.ok()).toBe(true);
182    });
183    it('should be abortable', async () => {
184      const { page, server } = getTestState();
185
186      await page.setRequestInterception(true);
187      page.on('request', (request) => {
188        if (request.url().endsWith('.css')) request.abort();
189        else request.continue();
190      });
191      let failedRequests = 0;
192      page.on('requestfailed', () => ++failedRequests);
193      const response = await page.goto(server.PREFIX + '/one-style.html');
194      expect(response.ok()).toBe(true);
195      expect(response.request().failure()).toBe(null);
196      expect(failedRequests).toBe(1);
197    });
198    it('should be abortable with custom error codes', async () => {
199      const { page, server } = getTestState();
200
201      await page.setRequestInterception(true);
202      page.on('request', (request) => {
203        request.abort('internetdisconnected');
204      });
205      let failedRequest = null;
206      page.on('requestfailed', (request) => (failedRequest = request));
207      await page.goto(server.EMPTY_PAGE).catch(() => {});
208      expect(failedRequest).toBeTruthy();
209      expect(failedRequest.failure().errorText).toBe(
210        'net::ERR_INTERNET_DISCONNECTED'
211      );
212    });
213    it('should send referer', async () => {
214      const { page, server } = getTestState();
215
216      await page.setExtraHTTPHeaders({
217        referer: 'http://google.com/',
218      });
219      await page.setRequestInterception(true);
220      page.on('request', (request) => request.continue());
221      const [request] = await Promise.all([
222        server.waitForRequest('/grid.html'),
223        page.goto(server.PREFIX + '/grid.html'),
224      ]);
225      expect(request.headers['referer']).toBe('http://google.com/');
226    });
227    it('should fail navigation when aborting main resource', async () => {
228      const { page, server, isChrome } = getTestState();
229
230      await page.setRequestInterception(true);
231      page.on('request', (request) => request.abort());
232      let error = null;
233      await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_));
234      expect(error).toBeTruthy();
235      if (isChrome) expect(error.message).toContain('net::ERR_FAILED');
236      else expect(error.message).toContain('NS_ERROR_FAILURE');
237    });
238    it('should work with redirects', async () => {
239      const { page, server } = getTestState();
240
241      await page.setRequestInterception(true);
242      const requests = [];
243      page.on('request', (request) => {
244        request.continue();
245        requests.push(request);
246      });
247      server.setRedirect(
248        '/non-existing-page.html',
249        '/non-existing-page-2.html'
250      );
251      server.setRedirect(
252        '/non-existing-page-2.html',
253        '/non-existing-page-3.html'
254      );
255      server.setRedirect(
256        '/non-existing-page-3.html',
257        '/non-existing-page-4.html'
258      );
259      server.setRedirect('/non-existing-page-4.html', '/empty.html');
260      const response = await page.goto(
261        server.PREFIX + '/non-existing-page.html'
262      );
263      expect(response.status()).toBe(200);
264      expect(response.url()).toContain('empty.html');
265      expect(requests.length).toBe(5);
266      expect(requests[2].resourceType()).toBe('document');
267      // Check redirect chain
268      const redirectChain = response.request().redirectChain();
269      expect(redirectChain.length).toBe(4);
270      expect(redirectChain[0].url()).toContain('/non-existing-page.html');
271      expect(redirectChain[2].url()).toContain('/non-existing-page-3.html');
272      for (let i = 0; i < redirectChain.length; ++i) {
273        const request = redirectChain[i];
274        expect(request.isNavigationRequest()).toBe(true);
275        expect(request.redirectChain().indexOf(request)).toBe(i);
276      }
277    });
278    it('should work with redirects for subresources', async () => {
279      const { page, server } = getTestState();
280
281      await page.setRequestInterception(true);
282      const requests = [];
283      page.on('request', (request) => {
284        request.continue();
285        if (!utils.isFavicon(request)) requests.push(request);
286      });
287      server.setRedirect('/one-style.css', '/two-style.css');
288      server.setRedirect('/two-style.css', '/three-style.css');
289      server.setRedirect('/three-style.css', '/four-style.css');
290      server.setRoute('/four-style.css', (req, res) =>
291        res.end('body {box-sizing: border-box; }')
292      );
293
294      const response = await page.goto(server.PREFIX + '/one-style.html');
295      expect(response.status()).toBe(200);
296      expect(response.url()).toContain('one-style.html');
297      expect(requests.length).toBe(5);
298      expect(requests[0].resourceType()).toBe('document');
299      expect(requests[1].resourceType()).toBe('stylesheet');
300      // Check redirect chain
301      const redirectChain = requests[1].redirectChain();
302      expect(redirectChain.length).toBe(3);
303      expect(redirectChain[0].url()).toContain('/one-style.css');
304      expect(redirectChain[2].url()).toContain('/three-style.css');
305    });
306    it('should be able to abort redirects', async () => {
307      const { page, server, isChrome } = getTestState();
308
309      await page.setRequestInterception(true);
310      server.setRedirect('/non-existing.json', '/non-existing-2.json');
311      server.setRedirect('/non-existing-2.json', '/simple.html');
312      page.on('request', (request) => {
313        if (request.url().includes('non-existing-2')) request.abort();
314        else request.continue();
315      });
316      await page.goto(server.EMPTY_PAGE);
317      const result = await page.evaluate(async () => {
318        try {
319          await fetch('/non-existing.json');
320        } catch (error) {
321          return error.message;
322        }
323      });
324      if (isChrome) expect(result).toContain('Failed to fetch');
325      else expect(result).toContain('NetworkError');
326    });
327    it('should work with equal requests', async () => {
328      const { page, server } = getTestState();
329
330      await page.goto(server.EMPTY_PAGE);
331      let responseCount = 1;
332      server.setRoute('/zzz', (req, res) => res.end(responseCount++ * 11 + ''));
333      await page.setRequestInterception(true);
334
335      let spinner = false;
336      // Cancel 2nd request.
337      page.on('request', (request) => {
338        if (utils.isFavicon(request)) {
339          request.continue();
340          return;
341        }
342        spinner ? request.abort() : request.continue();
343        spinner = !spinner;
344      });
345      const results = await page.evaluate(() =>
346        Promise.all([
347          fetch('/zzz')
348            .then((response) => response.text())
349            .catch(() => 'FAILED'),
350          fetch('/zzz')
351            .then((response) => response.text())
352            .catch(() => 'FAILED'),
353          fetch('/zzz')
354            .then((response) => response.text())
355            .catch(() => 'FAILED'),
356        ])
357      );
358      expect(results).toEqual(['11', 'FAILED', '22']);
359    });
360    it('should navigate to dataURL and fire dataURL requests', async () => {
361      const { page } = getTestState();
362
363      await page.setRequestInterception(true);
364      const requests = [];
365      page.on('request', (request) => {
366        requests.push(request);
367        request.continue();
368      });
369      const dataURL = 'data:text/html,<div>yo</div>';
370      const response = await page.goto(dataURL);
371      expect(response.status()).toBe(200);
372      expect(requests.length).toBe(1);
373      expect(requests[0].url()).toBe(dataURL);
374    });
375    it('should be able to fetch dataURL and fire dataURL requests', async () => {
376      const { page, server } = getTestState();
377
378      await page.goto(server.EMPTY_PAGE);
379      await page.setRequestInterception(true);
380      const requests = [];
381      page.on('request', (request) => {
382        requests.push(request);
383        request.continue();
384      });
385      const dataURL = 'data:text/html,<div>yo</div>';
386      const text = await page.evaluate(
387        (url: string) => fetch(url).then((r) => r.text()),
388        dataURL
389      );
390      expect(text).toBe('<div>yo</div>');
391      expect(requests.length).toBe(1);
392      expect(requests[0].url()).toBe(dataURL);
393    });
394    it('should navigate to URL with hash and fire requests without hash', async () => {
395      const { page, server } = getTestState();
396
397      await page.setRequestInterception(true);
398      const requests = [];
399      page.on('request', (request) => {
400        requests.push(request);
401        request.continue();
402      });
403      const response = await page.goto(server.EMPTY_PAGE + '#hash');
404      expect(response.status()).toBe(200);
405      expect(response.url()).toBe(server.EMPTY_PAGE);
406      expect(requests.length).toBe(1);
407      expect(requests[0].url()).toBe(server.EMPTY_PAGE);
408    });
409    it('should work with encoded server', async () => {
410      const { page, server } = getTestState();
411
412      // The requestWillBeSent will report encoded URL, whereas interception will
413      // report URL as-is. @see crbug.com/759388
414      await page.setRequestInterception(true);
415      page.on('request', (request) => request.continue());
416      const response = await page.goto(
417        server.PREFIX + '/some nonexisting page'
418      );
419      expect(response.status()).toBe(404);
420    });
421    it('should work with badly encoded server', async () => {
422      const { page, server } = getTestState();
423
424      await page.setRequestInterception(true);
425      server.setRoute('/malformed?rnd=%911', (req, res) => res.end());
426      page.on('request', (request) => request.continue());
427      const response = await page.goto(server.PREFIX + '/malformed?rnd=%911');
428      expect(response.status()).toBe(200);
429    });
430    it('should work with encoded server - 2', async () => {
431      const { page, server } = getTestState();
432
433      // The requestWillBeSent will report URL as-is, whereas interception will
434      // report encoded URL for stylesheet. @see crbug.com/759388
435      await page.setRequestInterception(true);
436      const requests = [];
437      page.on('request', (request) => {
438        request.continue();
439        requests.push(request);
440      });
441      const response = await page.goto(
442        `data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>`
443      );
444      expect(response.status()).toBe(200);
445      expect(requests.length).toBe(2);
446      expect(requests[1].response().status()).toBe(404);
447    });
448    it('should not throw "Invalid Interception Id" if the request was cancelled', async () => {
449      const { page, server } = getTestState();
450
451      await page.setContent('<iframe></iframe>');
452      await page.setRequestInterception(true);
453      let request = null;
454      page.on('request', async (r) => (request = r));
455      page.$eval(
456        'iframe',
457        (frame: HTMLIFrameElement, url: string) => (frame.src = url),
458        server.EMPTY_PAGE
459      ),
460        // Wait for request interception.
461        await utils.waitEvent(page, 'request');
462      // Delete frame to cause request to be canceled.
463      await page.$eval('iframe', (frame) => frame.remove());
464      let error = null;
465      await request.continue().catch((error_) => (error = error_));
466      expect(error).toBe(null);
467    });
468    it('should throw if interception is not enabled', async () => {
469      const { page, server } = getTestState();
470
471      let error = null;
472      page.on('request', async (request) => {
473        try {
474          await request.continue();
475        } catch (error_) {
476          error = error_;
477        }
478      });
479      await page.goto(server.EMPTY_PAGE);
480      expect(error.message).toContain('Request Interception is not enabled');
481    });
482    it('should work with file URLs', async () => {
483      const { page } = getTestState();
484
485      await page.setRequestInterception(true);
486      const urls = new Set();
487      page.on('request', (request) => {
488        urls.add(request.url().split('/').pop());
489        request.continue();
490      });
491      await page.goto(
492        pathToFileURL(path.join(__dirname, 'assets', 'one-style.html'))
493      );
494      expect(urls.size).toBe(2);
495      expect(urls.has('one-style.html')).toBe(true);
496      expect(urls.has('one-style.css')).toBe(true);
497    });
498    it('should not cache if cache disabled', async () => {
499      const { page, server } = getTestState();
500
501      // Load and re-load to make sure it's cached.
502      await page.goto(server.PREFIX + '/cached/one-style.html');
503
504      await page.setRequestInterception(true);
505      await page.setCacheEnabled(false);
506      page.on('request', (request) => request.continue());
507
508      const cached = [];
509      page.on('requestservedfromcache', (r) => cached.push(r));
510
511      await page.reload();
512      expect(cached.length).toBe(0);
513    });
514    it('should cache if cache enabled', async () => {
515      const { page, server } = getTestState();
516
517      // Load and re-load to make sure it's cached.
518      await page.goto(server.PREFIX + '/cached/one-style.html');
519
520      await page.setRequestInterception(true);
521      await page.setCacheEnabled(true);
522      page.on('request', (request) => request.continue());
523
524      const cached = [];
525      page.on('requestservedfromcache', (r) => cached.push(r));
526
527      await page.reload();
528      expect(cached.length).toBe(1);
529    });
530    it('should load fonts if cache enabled', async () => {
531      const { page, server } = getTestState();
532
533      await page.setRequestInterception(true);
534      await page.setCacheEnabled(true);
535      page.on('request', (request) => request.continue());
536
537      await page.goto(server.PREFIX + '/cached/one-style-font.html');
538      await page.waitForResponse((r) => r.url().endsWith('/one-style.woff'));
539    });
540  });
541
542  describe('Request.continue', function () {
543    it('should work', async () => {
544      const { page, server } = getTestState();
545
546      await page.setRequestInterception(true);
547      page.on('request', (request) => request.continue());
548      await page.goto(server.EMPTY_PAGE);
549    });
550    it('should amend HTTP headers', async () => {
551      const { page, server } = getTestState();
552
553      await page.setRequestInterception(true);
554      page.on('request', (request) => {
555        const headers = Object.assign({}, request.headers());
556        headers['FOO'] = 'bar';
557        request.continue({ headers });
558      });
559      await page.goto(server.EMPTY_PAGE);
560      const [request] = await Promise.all([
561        server.waitForRequest('/sleep.zzz'),
562        page.evaluate(() => fetch('/sleep.zzz')),
563      ]);
564      expect(request.headers['foo']).toBe('bar');
565    });
566    it('should redirect in a way non-observable to page', async () => {
567      const { page, server } = getTestState();
568
569      await page.setRequestInterception(true);
570      page.on('request', (request) => {
571        const redirectURL = request.url().includes('/empty.html')
572          ? server.PREFIX + '/consolelog.html'
573          : undefined;
574        request.continue({ url: redirectURL });
575      });
576      let consoleMessage = null;
577      page.on('console', (msg) => (consoleMessage = msg));
578      await page.goto(server.EMPTY_PAGE);
579      expect(page.url()).toBe(server.EMPTY_PAGE);
580      expect(consoleMessage.text()).toBe('yellow');
581    });
582    it('should amend method', async () => {
583      const { page, server } = getTestState();
584
585      await page.goto(server.EMPTY_PAGE);
586
587      await page.setRequestInterception(true);
588      page.on('request', (request) => {
589        request.continue({ method: 'POST' });
590      });
591      const [request] = await Promise.all([
592        server.waitForRequest('/sleep.zzz'),
593        page.evaluate(() => fetch('/sleep.zzz')),
594      ]);
595      expect(request.method).toBe('POST');
596    });
597    it('should amend post data', async () => {
598      const { page, server } = getTestState();
599
600      await page.goto(server.EMPTY_PAGE);
601
602      await page.setRequestInterception(true);
603      page.on('request', (request) => {
604        request.continue({ postData: 'doggo' });
605      });
606      const [serverRequest] = await Promise.all([
607        server.waitForRequest('/sleep.zzz'),
608        page.evaluate(() =>
609          fetch('/sleep.zzz', { method: 'POST', body: 'birdy' })
610        ),
611      ]);
612      expect(await serverRequest.postBody).toBe('doggo');
613    });
614    it('should amend both post data and method on navigation', async () => {
615      const { page, server } = getTestState();
616
617      await page.setRequestInterception(true);
618      page.on('request', (request) => {
619        request.continue({ method: 'POST', postData: 'doggo' });
620      });
621      const [serverRequest] = await Promise.all([
622        server.waitForRequest('/empty.html'),
623        page.goto(server.EMPTY_PAGE),
624      ]);
625      expect(serverRequest.method).toBe('POST');
626      expect(await serverRequest.postBody).toBe('doggo');
627    });
628  });
629
630  describe('Request.respond', function () {
631    it('should work', async () => {
632      const { page, server } = getTestState();
633
634      await page.setRequestInterception(true);
635      page.on('request', (request) => {
636        request.respond({
637          status: 201,
638          headers: {
639            foo: 'bar',
640          },
641          body: 'Yo, page!',
642        });
643      });
644      const response = await page.goto(server.EMPTY_PAGE);
645      expect(response.status()).toBe(201);
646      expect(response.headers().foo).toBe('bar');
647      expect(await page.evaluate(() => document.body.textContent)).toBe(
648        'Yo, page!'
649      );
650    });
651    it('should work with status code 422', async () => {
652      const { page, server } = getTestState();
653
654      await page.setRequestInterception(true);
655      page.on('request', (request) => {
656        request.respond({
657          status: 422,
658          body: 'Yo, page!',
659        });
660      });
661      const response = await page.goto(server.EMPTY_PAGE);
662      expect(response.status()).toBe(422);
663      expect(response.statusText()).toBe('Unprocessable Entity');
664      expect(await page.evaluate(() => document.body.textContent)).toBe(
665        'Yo, page!'
666      );
667    });
668    it('should redirect', async () => {
669      const { page, server } = getTestState();
670
671      await page.setRequestInterception(true);
672      page.on('request', (request) => {
673        if (!request.url().includes('rrredirect')) {
674          request.continue();
675          return;
676        }
677        request.respond({
678          status: 302,
679          headers: {
680            location: server.EMPTY_PAGE,
681          },
682        });
683      });
684      const response = await page.goto(server.PREFIX + '/rrredirect');
685      expect(response.request().redirectChain().length).toBe(1);
686      expect(response.request().redirectChain()[0].url()).toBe(
687        server.PREFIX + '/rrredirect'
688      );
689      expect(response.url()).toBe(server.EMPTY_PAGE);
690    });
691    it('should allow mocking binary responses', async () => {
692      const { page, server } = getTestState();
693
694      await page.setRequestInterception(true);
695      page.on('request', (request) => {
696        const imageBuffer = fs.readFileSync(
697          path.join(__dirname, 'assets', 'pptr.png')
698        );
699        request.respond({
700          contentType: 'image/png',
701          body: imageBuffer,
702        });
703      });
704      await page.evaluate((PREFIX) => {
705        const img = document.createElement('img');
706        img.src = PREFIX + '/does-not-exist.png';
707        document.body.appendChild(img);
708        return new Promise((fulfill) => (img.onload = fulfill));
709      }, server.PREFIX);
710      const img = await page.$('img');
711      expect(await img.screenshot()).toBeGolden('mock-binary-response.png');
712    });
713    it('should stringify intercepted request response headers', async () => {
714      const { page, server } = getTestState();
715
716      await page.setRequestInterception(true);
717      page.on('request', (request) => {
718        request.respond({
719          status: 200,
720          headers: {
721            foo: true,
722          },
723          body: 'Yo, page!',
724        });
725      });
726      const response = await page.goto(server.EMPTY_PAGE);
727      expect(response.status()).toBe(200);
728      const headers = response.headers();
729      expect(headers.foo).toBe('true');
730      expect(await page.evaluate(() => document.body.textContent)).toBe(
731        'Yo, page!'
732      );
733    });
734  });
735});
736
737/**
738 * @param {string} path
739 * @returns {string}
740 */
741function pathToFileURL(path) {
742  let pathName = path.replace(/\\/g, '/');
743  // Windows drive letter must be prefixed with a slash.
744  if (!pathName.startsWith('/')) pathName = '/' + pathName;
745  return 'file://' + pathName;
746}
747