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 expect from 'expect';
18import {
19  getTestState,
20  setupTestBrowserHooks,
21  setupTestPageAndContextHooks,
22  itFailsFirefox,
23  describeFailsFirefox,
24} from './mocha-utils'; // eslint-disable-line import/extensions
25
26describe('Emulation', () => {
27  setupTestBrowserHooks();
28  setupTestPageAndContextHooks();
29  let iPhone;
30  let iPhoneLandscape;
31
32  before(() => {
33    const { puppeteer } = getTestState();
34    iPhone = puppeteer.devices['iPhone 6'];
35    iPhoneLandscape = puppeteer.devices['iPhone 6 landscape'];
36  });
37
38  describe('Page.viewport', function () {
39    it('should get the proper viewport size', async () => {
40      const { page } = getTestState();
41
42      expect(page.viewport()).toEqual({ width: 800, height: 600 });
43      await page.setViewport({ width: 123, height: 456 });
44      expect(page.viewport()).toEqual({ width: 123, height: 456 });
45    });
46    it('should support mobile emulation', async () => {
47      const { page, server } = getTestState();
48
49      await page.goto(server.PREFIX + '/mobile.html');
50      expect(await page.evaluate(() => window.innerWidth)).toBe(800);
51      await page.setViewport(iPhone.viewport);
52      expect(await page.evaluate(() => window.innerWidth)).toBe(375);
53      await page.setViewport({ width: 400, height: 300 });
54      expect(await page.evaluate(() => window.innerWidth)).toBe(400);
55    });
56    it('should support touch emulation', async () => {
57      const { page, server } = getTestState();
58
59      await page.goto(server.PREFIX + '/mobile.html');
60      expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
61      await page.setViewport(iPhone.viewport);
62      expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true);
63      expect(await page.evaluate(dispatchTouch)).toBe('Received touch');
64      await page.setViewport({ width: 100, height: 100 });
65      expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
66
67      function dispatchTouch() {
68        let fulfill;
69        const promise = new Promise((x) => (fulfill = x));
70        window.ontouchstart = () => {
71          fulfill('Received touch');
72        };
73        window.dispatchEvent(new Event('touchstart'));
74
75        fulfill('Did not receive touch');
76
77        return promise;
78      }
79    });
80    it('should be detectable by Modernizr', async () => {
81      const { page, server } = getTestState();
82
83      await page.goto(server.PREFIX + '/detect-touch.html');
84      expect(await page.evaluate(() => document.body.textContent.trim())).toBe(
85        'NO'
86      );
87      await page.setViewport(iPhone.viewport);
88      await page.goto(server.PREFIX + '/detect-touch.html');
89      expect(await page.evaluate(() => document.body.textContent.trim())).toBe(
90        'YES'
91      );
92    });
93    it('should detect touch when applying viewport with touches', async () => {
94      const { page, server } = getTestState();
95
96      await page.setViewport({ width: 800, height: 600, hasTouch: true });
97      await page.addScriptTag({ url: server.PREFIX + '/modernizr.js' });
98      expect(await page.evaluate(() => globalThis.Modernizr.touchevents)).toBe(
99        true
100      );
101    });
102    it('should support landscape emulation', async () => {
103      const { page, server } = getTestState();
104
105      await page.goto(server.PREFIX + '/mobile.html');
106      expect(await page.evaluate(() => screen.orientation.type)).toBe(
107        'portrait-primary'
108      );
109      await page.setViewport(iPhoneLandscape.viewport);
110      expect(await page.evaluate(() => screen.orientation.type)).toBe(
111        'landscape-primary'
112      );
113      await page.setViewport({ width: 100, height: 100 });
114      expect(await page.evaluate(() => screen.orientation.type)).toBe(
115        'portrait-primary'
116      );
117    });
118  });
119
120  describe('Page.emulate', function () {
121    it('should work', async () => {
122      const { page, server } = getTestState();
123
124      await page.goto(server.PREFIX + '/mobile.html');
125      await page.emulate(iPhone);
126      expect(await page.evaluate(() => window.innerWidth)).toBe(375);
127      expect(await page.evaluate(() => navigator.userAgent)).toContain(
128        'iPhone'
129      );
130    });
131    it('should support clicking', async () => {
132      const { page, server } = getTestState();
133
134      await page.emulate(iPhone);
135      await page.goto(server.PREFIX + '/input/button.html');
136      const button = await page.$('button');
137      await page.evaluate(
138        (button: HTMLElement) => (button.style.marginTop = '200px'),
139        button
140      );
141      await button.click();
142      expect(await page.evaluate(() => globalThis.result)).toBe('Clicked');
143    });
144  });
145
146  describe('Page.emulateMediaType', function () {
147    it('should work', async () => {
148      const { page } = getTestState();
149
150      expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
151        true
152      );
153      expect(await page.evaluate(() => matchMedia('print').matches)).toBe(
154        false
155      );
156      await page.emulateMediaType('print');
157      expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
158        false
159      );
160      expect(await page.evaluate(() => matchMedia('print').matches)).toBe(true);
161      await page.emulateMediaType(null);
162      expect(await page.evaluate(() => matchMedia('screen').matches)).toBe(
163        true
164      );
165      expect(await page.evaluate(() => matchMedia('print').matches)).toBe(
166        false
167      );
168    });
169    it('should throw in case of bad argument', async () => {
170      const { page } = getTestState();
171
172      let error = null;
173      await page.emulateMediaType('bad').catch((error_) => (error = error_));
174      expect(error.message).toBe('Unsupported media type: bad');
175    });
176  });
177
178  describe('Page.emulateMediaFeatures', function () {
179    it('should work', async () => {
180      const { page } = getTestState();
181
182      await page.emulateMediaFeatures([
183        { name: 'prefers-reduced-motion', value: 'reduce' },
184      ]);
185      expect(
186        await page.evaluate(
187          () => matchMedia('(prefers-reduced-motion: reduce)').matches
188        )
189      ).toBe(true);
190      expect(
191        await page.evaluate(
192          () => matchMedia('(prefers-reduced-motion: no-preference)').matches
193        )
194      ).toBe(false);
195      await page.emulateMediaFeatures([
196        { name: 'prefers-color-scheme', value: 'light' },
197      ]);
198      expect(
199        await page.evaluate(
200          () => matchMedia('(prefers-color-scheme: light)').matches
201        )
202      ).toBe(true);
203      expect(
204        await page.evaluate(
205          () => matchMedia('(prefers-color-scheme: dark)').matches
206        )
207      ).toBe(false);
208      await page.emulateMediaFeatures([
209        { name: 'prefers-color-scheme', value: 'dark' },
210      ]);
211      expect(
212        await page.evaluate(
213          () => matchMedia('(prefers-color-scheme: dark)').matches
214        )
215      ).toBe(true);
216      expect(
217        await page.evaluate(
218          () => matchMedia('(prefers-color-scheme: light)').matches
219        )
220      ).toBe(false);
221      await page.emulateMediaFeatures([
222        { name: 'prefers-reduced-motion', value: 'reduce' },
223        { name: 'prefers-color-scheme', value: 'light' },
224      ]);
225      expect(
226        await page.evaluate(
227          () => matchMedia('(prefers-reduced-motion: reduce)').matches
228        )
229      ).toBe(true);
230      expect(
231        await page.evaluate(
232          () => matchMedia('(prefers-reduced-motion: no-preference)').matches
233        )
234      ).toBe(false);
235      expect(
236        await page.evaluate(
237          () => matchMedia('(prefers-color-scheme: light)').matches
238        )
239      ).toBe(true);
240      expect(
241        await page.evaluate(
242          () => matchMedia('(prefers-color-scheme: dark)').matches
243        )
244      ).toBe(false);
245      await page.emulateMediaFeatures([{ name: 'color-gamut', value: 'srgb' }]);
246      expect(
247        await page.evaluate(() => matchMedia('(color-gamut: p3)').matches)
248      ).toBe(false);
249      expect(
250        await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches)
251      ).toBe(true);
252      expect(
253        await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches)
254      ).toBe(false);
255      await page.emulateMediaFeatures([{ name: 'color-gamut', value: 'p3' }]);
256      expect(
257        await page.evaluate(() => matchMedia('(color-gamut: p3)').matches)
258      ).toBe(true);
259      expect(
260        await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches)
261      ).toBe(true);
262      expect(
263        await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches)
264      ).toBe(false);
265      await page.emulateMediaFeatures([
266        { name: 'color-gamut', value: 'rec2020' },
267      ]);
268      expect(
269        await page.evaluate(() => matchMedia('(color-gamut: p3)').matches)
270      ).toBe(true);
271      expect(
272        await page.evaluate(() => matchMedia('(color-gamut: srgb)').matches)
273      ).toBe(true);
274      expect(
275        await page.evaluate(() => matchMedia('(color-gamut: rec2020)').matches)
276      ).toBe(true);
277    });
278    it('should throw in case of bad argument', async () => {
279      const { page } = getTestState();
280
281      let error = null;
282      await page
283        .emulateMediaFeatures([{ name: 'bad', value: '' }])
284        .catch((error_) => (error = error_));
285      expect(error.message).toBe('Unsupported media feature: bad');
286    });
287  });
288
289  describe('Page.emulateTimezone', function () {
290    it('should work', async () => {
291      const { page } = getTestState();
292
293      await page.evaluate(() => {
294        globalThis.date = new Date(1479579154987);
295      });
296      await page.emulateTimezone('America/Jamaica');
297      expect(await page.evaluate(() => globalThis.date.toString())).toBe(
298        'Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'
299      );
300
301      await page.emulateTimezone('Pacific/Honolulu');
302      expect(await page.evaluate(() => globalThis.date.toString())).toBe(
303        'Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)'
304      );
305
306      await page.emulateTimezone('America/Buenos_Aires');
307      expect(await page.evaluate(() => globalThis.date.toString())).toBe(
308        'Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'
309      );
310
311      await page.emulateTimezone('Europe/Berlin');
312      expect(await page.evaluate(() => globalThis.date.toString())).toBe(
313        'Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)'
314      );
315    });
316
317    it('should throw for invalid timezone IDs', async () => {
318      const { page } = getTestState();
319
320      let error = null;
321      await page.emulateTimezone('Foo/Bar').catch((error_) => (error = error_));
322      expect(error.message).toBe('Invalid timezone ID: Foo/Bar');
323      await page.emulateTimezone('Baz/Qux').catch((error_) => (error = error_));
324      expect(error.message).toBe('Invalid timezone ID: Baz/Qux');
325    });
326  });
327
328  describe('Page.emulateVisionDeficiency', function () {
329    it('should work', async () => {
330      const { page, server } = getTestState();
331
332      await page.setViewport({ width: 500, height: 500 });
333      await page.goto(server.PREFIX + '/grid.html');
334
335      {
336        await page.emulateVisionDeficiency('none');
337        const screenshot = await page.screenshot();
338        expect(screenshot).toBeGolden('screenshot-sanity.png');
339      }
340
341      {
342        await page.emulateVisionDeficiency('achromatopsia');
343        const screenshot = await page.screenshot();
344        expect(screenshot).toBeGolden('vision-deficiency-achromatopsia.png');
345      }
346
347      {
348        await page.emulateVisionDeficiency('blurredVision');
349        const screenshot = await page.screenshot();
350        expect(screenshot).toBeGolden('vision-deficiency-blurredVision.png');
351      }
352
353      {
354        await page.emulateVisionDeficiency('deuteranopia');
355        const screenshot = await page.screenshot();
356        expect(screenshot).toBeGolden('vision-deficiency-deuteranopia.png');
357      }
358
359      {
360        await page.emulateVisionDeficiency('protanopia');
361        const screenshot = await page.screenshot();
362        expect(screenshot).toBeGolden('vision-deficiency-protanopia.png');
363      }
364
365      {
366        await page.emulateVisionDeficiency('tritanopia');
367        const screenshot = await page.screenshot();
368        expect(screenshot).toBeGolden('vision-deficiency-tritanopia.png');
369      }
370
371      {
372        await page.emulateVisionDeficiency('none');
373        const screenshot = await page.screenshot();
374        expect(screenshot).toBeGolden('screenshot-sanity.png');
375      }
376    });
377
378    it('should throw for invalid vision deficiencies', async () => {
379      const { page } = getTestState();
380
381      let error = null;
382      await page
383        // @ts-expect-error deliberately passign invalid deficiency
384        .emulateVisionDeficiency('invalid')
385        .catch((error_) => (error = error_));
386      expect(error.message).toBe('Unsupported vision deficiency: invalid');
387    });
388  });
389
390  describe('Page.emulateNetworkConditions', function () {
391    it('should change navigator.connection.effectiveType', async () => {
392      const { page, puppeteer } = getTestState();
393
394      const slow3G = puppeteer.networkConditions['Slow 3G'];
395      const fast3G = puppeteer.networkConditions['Fast 3G'];
396
397      expect(
398        await page.evaluate('window.navigator.connection.effectiveType')
399      ).toBe('4g');
400      await page.emulateNetworkConditions(fast3G);
401      expect(
402        await page.evaluate('window.navigator.connection.effectiveType')
403      ).toBe('3g');
404      await page.emulateNetworkConditions(slow3G);
405      expect(
406        await page.evaluate('window.navigator.connection.effectiveType')
407      ).toBe('2g');
408      await page.emulateNetworkConditions(null);
409    });
410  });
411});
412