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 expect from 'expect'; 17import { 18 getTestState, 19 setupTestBrowserHooks, 20 setupTestPageAndContextHooks, 21} from './mocha-utils'; // eslint-disable-line import/extensions 22import { CustomQueryHandler } from '../lib/cjs/puppeteer/common/QueryHandler.js'; 23 24describe('querySelector', function () { 25 setupTestBrowserHooks(); 26 setupTestPageAndContextHooks(); 27 describe('Page.$eval', function () { 28 it('should work', async () => { 29 const { page } = getTestState(); 30 31 await page.setContent('<section id="testAttribute">43543</section>'); 32 const idAttribute = await page.$eval('section', (e) => e.id); 33 expect(idAttribute).toBe('testAttribute'); 34 }); 35 it('should accept arguments', async () => { 36 const { page } = getTestState(); 37 38 await page.setContent('<section>hello</section>'); 39 const text = await page.$eval( 40 'section', 41 (e, suffix) => e.textContent + suffix, 42 ' world!' 43 ); 44 expect(text).toBe('hello world!'); 45 }); 46 it('should accept ElementHandles as arguments', async () => { 47 const { page } = getTestState(); 48 49 await page.setContent('<section>hello</section><div> world</div>'); 50 const divHandle = await page.$('div'); 51 const text = await page.$eval( 52 'section', 53 (e, div: HTMLElement) => e.textContent + div.textContent, 54 divHandle 55 ); 56 expect(text).toBe('hello world'); 57 }); 58 it('should throw error if no element is found', async () => { 59 const { page } = getTestState(); 60 61 let error = null; 62 await page 63 .$eval('section', (e) => e.id) 64 .catch((error_) => (error = error_)); 65 expect(error.message).toContain( 66 'failed to find element matching selector "section"' 67 ); 68 }); 69 }); 70 71 describe('pierceHandler', function () { 72 beforeEach(async () => { 73 const { page } = getTestState(); 74 await page.setContent( 75 `<script> 76 const div = document.createElement('div'); 77 const shadowRoot = div.attachShadow({mode: 'open'}); 78 const div1 = document.createElement('div'); 79 div1.textContent = 'Hello'; 80 div1.className = 'foo'; 81 const div2 = document.createElement('div'); 82 div2.textContent = 'World'; 83 div2.className = 'foo'; 84 shadowRoot.appendChild(div1); 85 shadowRoot.appendChild(div2); 86 document.documentElement.appendChild(div); 87 </script>` 88 ); 89 }); 90 it('should find first element in shadow', async () => { 91 const { page } = getTestState(); 92 const div = await page.$('pierce/.foo'); 93 const text = await div.evaluate( 94 (element: Element) => element.textContent 95 ); 96 expect(text).toBe('Hello'); 97 }); 98 it('should find all elements in shadow', async () => { 99 const { page } = getTestState(); 100 const divs = await page.$$('pierce/.foo'); 101 const text = await Promise.all( 102 divs.map((div) => 103 div.evaluate((element: Element) => element.textContent) 104 ) 105 ); 106 expect(text.join(' ')).toBe('Hello World'); 107 }); 108 }); 109 110 // The tests for $$eval are repeated later in this file in the test group 'QueryAll'. 111 // This is done to also test a query handler where QueryAll returns an Element[] 112 // as opposed to NodeListOf<Element>. 113 describe('Page.$$eval', function () { 114 it('should work', async () => { 115 const { page } = getTestState(); 116 117 await page.setContent( 118 '<div>hello</div><div>beautiful</div><div>world!</div>' 119 ); 120 const divsCount = await page.$$eval('div', (divs) => divs.length); 121 expect(divsCount).toBe(3); 122 }); 123 it('should accept extra arguments', async () => { 124 const { page } = getTestState(); 125 await page.setContent( 126 '<div>hello</div><div>beautiful</div><div>world!</div>' 127 ); 128 const divsCountPlus5 = await page.$$eval( 129 'div', 130 (divs, two: number, three: number) => divs.length + two + three, 131 2, 132 3 133 ); 134 expect(divsCountPlus5).toBe(8); 135 }); 136 it('should accept ElementHandles as arguments', async () => { 137 const { page } = getTestState(); 138 await page.setContent( 139 '<section>2</section><section>2</section><section>1</section><div>3</div>' 140 ); 141 const divHandle = await page.$('div'); 142 const sum = await page.$$eval( 143 'section', 144 (sections, div: HTMLElement) => 145 sections.reduce( 146 (acc, section) => acc + Number(section.textContent), 147 0 148 ) + Number(div.textContent), 149 divHandle 150 ); 151 expect(sum).toBe(8); 152 }); 153 it('should handle many elements', async () => { 154 const { page } = getTestState(); 155 await page.evaluate( 156 ` 157 for (var i = 0; i <= 1000; i++) { 158 const section = document.createElement('section'); 159 section.textContent = i; 160 document.body.appendChild(section); 161 } 162 ` 163 ); 164 const sum = await page.$$eval('section', (sections) => 165 sections.reduce((acc, section) => acc + Number(section.textContent), 0) 166 ); 167 expect(sum).toBe(500500); 168 }); 169 }); 170 171 describe('Page.$', function () { 172 it('should query existing element', async () => { 173 const { page } = getTestState(); 174 175 await page.setContent('<section>test</section>'); 176 const element = await page.$('section'); 177 expect(element).toBeTruthy(); 178 }); 179 it('should return null for non-existing element', async () => { 180 const { page } = getTestState(); 181 182 const element = await page.$('non-existing-element'); 183 expect(element).toBe(null); 184 }); 185 }); 186 187 describe('Page.$$', function () { 188 it('should query existing elements', async () => { 189 const { page } = getTestState(); 190 191 await page.setContent('<div>A</div><br/><div>B</div>'); 192 const elements = await page.$$('div'); 193 expect(elements.length).toBe(2); 194 const promises = elements.map((element) => 195 page.evaluate((e: HTMLElement) => e.textContent, element) 196 ); 197 expect(await Promise.all(promises)).toEqual(['A', 'B']); 198 }); 199 it('should return empty array if nothing is found', async () => { 200 const { page, server } = getTestState(); 201 202 await page.goto(server.EMPTY_PAGE); 203 const elements = await page.$$('div'); 204 expect(elements.length).toBe(0); 205 }); 206 }); 207 208 describe('Path.$x', function () { 209 it('should query existing element', async () => { 210 const { page } = getTestState(); 211 212 await page.setContent('<section>test</section>'); 213 const elements = await page.$x('/html/body/section'); 214 expect(elements[0]).toBeTruthy(); 215 expect(elements.length).toBe(1); 216 }); 217 it('should return empty array for non-existing element', async () => { 218 const { page } = getTestState(); 219 220 const element = await page.$x('/html/body/non-existing-element'); 221 expect(element).toEqual([]); 222 }); 223 it('should return multiple elements', async () => { 224 const { page } = getTestState(); 225 226 await page.setContent('<div></div><div></div>'); 227 const elements = await page.$x('/html/body/div'); 228 expect(elements.length).toBe(2); 229 }); 230 }); 231 232 describe('ElementHandle.$', function () { 233 it('should query existing element', async () => { 234 const { page, server } = getTestState(); 235 236 await page.goto(server.PREFIX + '/playground.html'); 237 await page.setContent( 238 '<html><body><div class="second"><div class="inner">A</div></div></body></html>' 239 ); 240 const html = await page.$('html'); 241 const second = await html.$('.second'); 242 const inner = await second.$('.inner'); 243 const content = await page.evaluate( 244 (e: HTMLElement) => e.textContent, 245 inner 246 ); 247 expect(content).toBe('A'); 248 }); 249 250 it('should return null for non-existing element', async () => { 251 const { page } = getTestState(); 252 253 await page.setContent( 254 '<html><body><div class="second"><div class="inner">B</div></div></body></html>' 255 ); 256 const html = await page.$('html'); 257 const second = await html.$('.third'); 258 expect(second).toBe(null); 259 }); 260 }); 261 describe('ElementHandle.$eval', function () { 262 it('should work', async () => { 263 const { page } = getTestState(); 264 265 await page.setContent( 266 '<html><body><div class="tweet"><div class="like">100</div><div class="retweets">10</div></div></body></html>' 267 ); 268 const tweet = await page.$('.tweet'); 269 const content = await tweet.$eval( 270 '.like', 271 (node: HTMLElement) => node.innerText 272 ); 273 expect(content).toBe('100'); 274 }); 275 276 it('should retrieve content from subtree', async () => { 277 const { page } = getTestState(); 278 279 const htmlContent = 280 '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a-child-div</div></div>'; 281 await page.setContent(htmlContent); 282 const elementHandle = await page.$('#myId'); 283 const content = await elementHandle.$eval( 284 '.a', 285 (node: HTMLElement) => node.innerText 286 ); 287 expect(content).toBe('a-child-div'); 288 }); 289 290 it('should throw in case of missing selector', async () => { 291 const { page } = getTestState(); 292 293 const htmlContent = 294 '<div class="a">not-a-child-div</div><div id="myId"></div>'; 295 await page.setContent(htmlContent); 296 const elementHandle = await page.$('#myId'); 297 const errorMessage = await elementHandle 298 .$eval('.a', (node: HTMLElement) => node.innerText) 299 .catch((error) => error.message); 300 expect(errorMessage).toBe( 301 `Error: failed to find element matching selector ".a"` 302 ); 303 }); 304 }); 305 describe('ElementHandle.$$eval', function () { 306 it('should work', async () => { 307 const { page } = getTestState(); 308 309 await page.setContent( 310 '<html><body><div class="tweet"><div class="like">100</div><div class="like">10</div></div></body></html>' 311 ); 312 const tweet = await page.$('.tweet'); 313 const content = await tweet.$$eval('.like', (nodes: HTMLElement[]) => 314 nodes.map((n) => n.innerText) 315 ); 316 expect(content).toEqual(['100', '10']); 317 }); 318 319 it('should retrieve content from subtree', async () => { 320 const { page } = getTestState(); 321 322 const htmlContent = 323 '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a1-child-div</div><div class="a">a2-child-div</div></div>'; 324 await page.setContent(htmlContent); 325 const elementHandle = await page.$('#myId'); 326 const content = await elementHandle.$$eval('.a', (nodes: HTMLElement[]) => 327 nodes.map((n) => n.innerText) 328 ); 329 expect(content).toEqual(['a1-child-div', 'a2-child-div']); 330 }); 331 332 it('should not throw in case of missing selector', async () => { 333 const { page } = getTestState(); 334 335 const htmlContent = 336 '<div class="a">not-a-child-div</div><div id="myId"></div>'; 337 await page.setContent(htmlContent); 338 const elementHandle = await page.$('#myId'); 339 const nodesLength = await elementHandle.$$eval( 340 '.a', 341 (nodes) => nodes.length 342 ); 343 expect(nodesLength).toBe(0); 344 }); 345 }); 346 347 describe('ElementHandle.$$', function () { 348 it('should query existing elements', async () => { 349 const { page } = getTestState(); 350 351 await page.setContent( 352 '<html><body><div>A</div><br/><div>B</div></body></html>' 353 ); 354 const html = await page.$('html'); 355 const elements = await html.$$('div'); 356 expect(elements.length).toBe(2); 357 const promises = elements.map((element) => 358 page.evaluate((e: HTMLElement) => e.textContent, element) 359 ); 360 expect(await Promise.all(promises)).toEqual(['A', 'B']); 361 }); 362 363 it('should return empty array for non-existing elements', async () => { 364 const { page } = getTestState(); 365 366 await page.setContent( 367 '<html><body><span>A</span><br/><span>B</span></body></html>' 368 ); 369 const html = await page.$('html'); 370 const elements = await html.$$('div'); 371 expect(elements.length).toBe(0); 372 }); 373 }); 374 375 describe('ElementHandle.$x', function () { 376 it('should query existing element', async () => { 377 const { page, server } = getTestState(); 378 379 await page.goto(server.PREFIX + '/playground.html'); 380 await page.setContent( 381 '<html><body><div class="second"><div class="inner">A</div></div></body></html>' 382 ); 383 const html = await page.$('html'); 384 const second = await html.$x(`./body/div[contains(@class, 'second')]`); 385 const inner = await second[0].$x(`./div[contains(@class, 'inner')]`); 386 const content = await page.evaluate( 387 (e: HTMLElement) => e.textContent, 388 inner[0] 389 ); 390 expect(content).toBe('A'); 391 }); 392 393 it('should return null for non-existing element', async () => { 394 const { page } = getTestState(); 395 396 await page.setContent( 397 '<html><body><div class="second"><div class="inner">B</div></div></body></html>' 398 ); 399 const html = await page.$('html'); 400 const second = await html.$x(`/div[contains(@class, 'third')]`); 401 expect(second).toEqual([]); 402 }); 403 }); 404 405 // This is the same tests for `$$eval` and `$$` as above, but with a queryAll 406 // handler that returns an array instead of a list of nodes. 407 describe('QueryAll', function () { 408 const handler: CustomQueryHandler = { 409 queryAll: (element: Element, selector: string) => 410 Array.from(element.querySelectorAll(selector)), 411 }; 412 before(() => { 413 const { puppeteer } = getTestState(); 414 puppeteer.registerCustomQueryHandler('allArray', handler); 415 }); 416 417 it('should have registered handler', async () => { 418 const { puppeteer } = getTestState(); 419 expect( 420 puppeteer.customQueryHandlerNames().includes('allArray') 421 ).toBeTruthy(); 422 }); 423 it('$$ should query existing elements', async () => { 424 const { page } = getTestState(); 425 426 await page.setContent( 427 '<html><body><div>A</div><br/><div>B</div></body></html>' 428 ); 429 const html = await page.$('html'); 430 const elements = await html.$$('allArray/div'); 431 expect(elements.length).toBe(2); 432 const promises = elements.map((element) => 433 page.evaluate((e: HTMLElement) => e.textContent, element) 434 ); 435 expect(await Promise.all(promises)).toEqual(['A', 'B']); 436 }); 437 438 it('$$ should return empty array for non-existing elements', async () => { 439 const { page } = getTestState(); 440 441 await page.setContent( 442 '<html><body><span>A</span><br/><span>B</span></body></html>' 443 ); 444 const html = await page.$('html'); 445 const elements = await html.$$('allArray/div'); 446 expect(elements.length).toBe(0); 447 }); 448 it('$$eval should work', async () => { 449 const { page } = getTestState(); 450 451 await page.setContent( 452 '<div>hello</div><div>beautiful</div><div>world!</div>' 453 ); 454 const divsCount = await page.$$eval( 455 'allArray/div', 456 (divs) => divs.length 457 ); 458 expect(divsCount).toBe(3); 459 }); 460 it('$$eval should accept extra arguments', async () => { 461 const { page } = getTestState(); 462 await page.setContent( 463 '<div>hello</div><div>beautiful</div><div>world!</div>' 464 ); 465 const divsCountPlus5 = await page.$$eval( 466 'allArray/div', 467 (divs, two: number, three: number) => divs.length + two + three, 468 2, 469 3 470 ); 471 expect(divsCountPlus5).toBe(8); 472 }); 473 it('$$eval should accept ElementHandles as arguments', async () => { 474 const { page } = getTestState(); 475 await page.setContent( 476 '<section>2</section><section>2</section><section>1</section><div>3</div>' 477 ); 478 const divHandle = await page.$('div'); 479 const sum = await page.$$eval( 480 'allArray/section', 481 (sections, div: HTMLElement) => 482 sections.reduce( 483 (acc, section) => acc + Number(section.textContent), 484 0 485 ) + Number(div.textContent), 486 divHandle 487 ); 488 expect(sum).toBe(8); 489 }); 490 it('$$eval should handle many elements', async () => { 491 const { page } = getTestState(); 492 await page.evaluate( 493 ` 494 for (var i = 0; i <= 1000; i++) { 495 const section = document.createElement('section'); 496 section.textContent = i; 497 document.body.appendChild(section); 498 } 499 ` 500 ); 501 const sum = await page.$$eval('allArray/section', (sections) => 502 sections.reduce((acc, section) => acc + Number(section.textContent), 0) 503 ); 504 expect(sum).toBe(500500); 505 }); 506 }); 507}); 508