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 */ 16import os from 'os'; 17import expect from 'expect'; 18import { 19 getTestState, 20 setupTestBrowserHooks, 21 setupTestPageAndContextHooks, 22 itFailsFirefox, 23} from './mocha-utils'; // eslint-disable-line import/extensions 24import { KeyInput } from '../lib/cjs/puppeteer/common/USKeyboardLayout.js'; 25 26interface Dimensions { 27 x: number; 28 y: number; 29 width: number; 30 height: number; 31} 32 33function dimensions(): Dimensions { 34 const rect = document.querySelector('textarea').getBoundingClientRect(); 35 return { 36 x: rect.left, 37 y: rect.top, 38 width: rect.width, 39 height: rect.height, 40 }; 41} 42 43describe('Mouse', function () { 44 setupTestBrowserHooks(); 45 setupTestPageAndContextHooks(); 46 it('should click the document', async () => { 47 const { page } = getTestState(); 48 49 await page.evaluate(() => { 50 globalThis.clickPromise = new Promise((resolve) => { 51 document.addEventListener('click', (event) => { 52 resolve({ 53 type: event.type, 54 detail: event.detail, 55 clientX: event.clientX, 56 clientY: event.clientY, 57 isTrusted: event.isTrusted, 58 button: event.button, 59 }); 60 }); 61 }); 62 }); 63 await page.mouse.click(50, 60); 64 const event = await page.evaluate<() => MouseEvent>( 65 () => globalThis.clickPromise 66 ); 67 expect(event.type).toBe('click'); 68 expect(event.detail).toBe(1); 69 expect(event.clientX).toBe(50); 70 expect(event.clientY).toBe(60); 71 expect(event.isTrusted).toBe(true); 72 expect(event.button).toBe(0); 73 }); 74 it('should resize the textarea', async () => { 75 const { page, server } = getTestState(); 76 77 await page.goto(server.PREFIX + '/input/textarea.html'); 78 const { x, y, width, height } = await page.evaluate<() => Dimensions>( 79 dimensions 80 ); 81 const mouse = page.mouse; 82 await mouse.move(x + width - 4, y + height - 4); 83 await mouse.down(); 84 await mouse.move(x + width + 100, y + height + 100); 85 await mouse.up(); 86 const newDimensions = await page.evaluate<() => Dimensions>(dimensions); 87 expect(newDimensions.width).toBe(Math.round(width + 104)); 88 expect(newDimensions.height).toBe(Math.round(height + 104)); 89 }); 90 it('should select the text with mouse', async () => { 91 const { page, server } = getTestState(); 92 93 await page.goto(server.PREFIX + '/input/textarea.html'); 94 await page.focus('textarea'); 95 const text = 96 "This is the text that we are going to try to select. Let's see how it goes."; 97 await page.keyboard.type(text); 98 // Firefox needs an extra frame here after typing or it will fail to set the scrollTop 99 await page.evaluate(() => new Promise(requestAnimationFrame)); 100 await page.evaluate( 101 () => (document.querySelector('textarea').scrollTop = 0) 102 ); 103 const { x, y } = await page.evaluate(dimensions); 104 await page.mouse.move(x + 2, y + 2); 105 await page.mouse.down(); 106 await page.mouse.move(100, 100); 107 await page.mouse.up(); 108 expect( 109 await page.evaluate(() => { 110 const textarea = document.querySelector('textarea'); 111 return textarea.value.substring( 112 textarea.selectionStart, 113 textarea.selectionEnd 114 ); 115 }) 116 ).toBe(text); 117 }); 118 it('should trigger hover state', async () => { 119 const { page, server } = getTestState(); 120 121 await page.goto(server.PREFIX + '/input/scrollable.html'); 122 await page.hover('#button-6'); 123 expect( 124 await page.evaluate(() => document.querySelector('button:hover').id) 125 ).toBe('button-6'); 126 await page.hover('#button-2'); 127 expect( 128 await page.evaluate(() => document.querySelector('button:hover').id) 129 ).toBe('button-2'); 130 await page.hover('#button-91'); 131 expect( 132 await page.evaluate(() => document.querySelector('button:hover').id) 133 ).toBe('button-91'); 134 }); 135 it( 136 'should trigger hover state with removed window.Node', 137 async () => { 138 const { page, server } = getTestState(); 139 140 await page.goto(server.PREFIX + '/input/scrollable.html'); 141 await page.evaluate(() => delete window.Node); 142 await page.hover('#button-6'); 143 expect( 144 await page.evaluate(() => document.querySelector('button:hover').id) 145 ).toBe('button-6'); 146 } 147 ); 148 it('should set modifier keys on click', async () => { 149 const { page, server, isFirefox } = getTestState(); 150 151 await page.goto(server.PREFIX + '/input/scrollable.html'); 152 await page.evaluate(() => 153 document 154 .querySelector('#button-3') 155 .addEventListener('mousedown', (e) => (globalThis.lastEvent = e), true) 156 ); 157 const modifiers = new Map<KeyInput, string>([ 158 ['Shift', 'shiftKey'], 159 ['Control', 'ctrlKey'], 160 ['Alt', 'altKey'], 161 ['Meta', 'metaKey'], 162 ]); 163 // In Firefox, the Meta modifier only exists on Mac 164 if (isFirefox && os.platform() !== 'darwin') delete modifiers['Meta']; 165 for (const [modifier, key] of modifiers) { 166 await page.keyboard.down(modifier); 167 await page.click('#button-3'); 168 if ( 169 !(await page.evaluate((mod: string) => globalThis.lastEvent[mod], key)) 170 ) 171 throw new Error(key + ' should be true'); 172 await page.keyboard.up(modifier); 173 } 174 await page.click('#button-3'); 175 for (const [modifier, key] of modifiers) { 176 if (await page.evaluate((mod: string) => globalThis.lastEvent[mod], key)) 177 throw new Error(modifiers[modifier] + ' should be false'); 178 } 179 }); 180 it('should send mouse wheel events', async () => { 181 const { page, server } = getTestState(); 182 183 await page.goto(server.PREFIX + '/input/wheel.html'); 184 const elem = await page.$('div'); 185 const boundingBoxBefore = await elem.boundingBox(); 186 expect(boundingBoxBefore).toMatchObject({ 187 width: 115, 188 height: 115, 189 }); 190 191 await page.mouse.move( 192 boundingBoxBefore.x + boundingBoxBefore.width / 2, 193 boundingBoxBefore.y + boundingBoxBefore.height / 2 194 ); 195 196 await page.mouse.wheel({ deltaY: -100 }); 197 const boundingBoxAfter = await elem.boundingBox(); 198 expect(boundingBoxAfter).toMatchObject({ 199 width: 230, 200 height: 230, 201 }); 202 }); 203 it('should tween mouse movement', async () => { 204 const { page } = getTestState(); 205 206 await page.mouse.move(100, 100); 207 await page.evaluate(() => { 208 globalThis.result = []; 209 document.addEventListener('mousemove', (event) => { 210 globalThis.result.push([event.clientX, event.clientY]); 211 }); 212 }); 213 await page.mouse.move(200, 300, { steps: 5 }); 214 expect(await page.evaluate('result')).toEqual([ 215 [120, 140], 216 [140, 180], 217 [160, 220], 218 [180, 260], 219 [200, 300], 220 ]); 221 }); 222 // @see https://crbug.com/929806 223 it( 224 'should work with mobile viewports and cross process navigations', 225 async () => { 226 const { page, server } = getTestState(); 227 228 await page.goto(server.EMPTY_PAGE); 229 await page.setViewport({ width: 360, height: 640, isMobile: true }); 230 await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html'); 231 await page.evaluate(() => { 232 document.addEventListener('click', (event) => { 233 globalThis.result = { x: event.clientX, y: event.clientY }; 234 }); 235 }); 236 237 await page.mouse.click(30, 40); 238 239 expect(await page.evaluate('result')).toEqual({ x: 30, y: 40 }); 240 } 241 ); 242}); 243