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