DOMWorld.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. /**
  2. * Copyright 2019 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. const fs = require('fs');
  17. const {helper, assert} = require('./helper');
  18. const {LifecycleWatcher} = require('./LifecycleWatcher');
  19. const {TimeoutError} = require('./Errors');
  20. const readFileAsync = helper.promisify(fs.readFile);
  21. /**
  22. * @unrestricted
  23. */
  24. class DOMWorld {
  25. /**
  26. * @param {!Puppeteer.FrameManager} frameManager
  27. * @param {!Puppeteer.Frame} frame
  28. * @param {!Puppeteer.TimeoutSettings} timeoutSettings
  29. */
  30. constructor(frameManager, frame, timeoutSettings) {
  31. this._frameManager = frameManager;
  32. this._frame = frame;
  33. this._timeoutSettings = timeoutSettings;
  34. /** @type {?Promise<!Puppeteer.ElementHandle>} */
  35. this._documentPromise = null;
  36. /** @type {!Promise<!Puppeteer.ExecutionContext>} */
  37. this._contextPromise;
  38. this._contextResolveCallback = null;
  39. this._setContext(null);
  40. /** @type {!Set<!WaitTask>} */
  41. this._waitTasks = new Set();
  42. this._detached = false;
  43. }
  44. /**
  45. * @return {!Puppeteer.Frame}
  46. */
  47. frame() {
  48. return this._frame;
  49. }
  50. /**
  51. * @param {?Puppeteer.ExecutionContext} context
  52. */
  53. _setContext(context) {
  54. if (context) {
  55. this._contextResolveCallback.call(null, context);
  56. this._contextResolveCallback = null;
  57. for (const waitTask of this._waitTasks)
  58. waitTask.rerun();
  59. } else {
  60. this._documentPromise = null;
  61. this._contextPromise = new Promise(fulfill => {
  62. this._contextResolveCallback = fulfill;
  63. });
  64. }
  65. }
  66. /**
  67. * @return {boolean}
  68. */
  69. _hasContext() {
  70. return !this._contextResolveCallback;
  71. }
  72. _detach() {
  73. this._detached = true;
  74. for (const waitTask of this._waitTasks)
  75. waitTask.terminate(new Error('waitForFunction failed: frame got detached.'));
  76. }
  77. /**
  78. * @return {!Promise<!Puppeteer.ExecutionContext>}
  79. */
  80. executionContext() {
  81. if (this._detached)
  82. throw new Error(`Execution Context is not available in detached frame "${this._frame.url()}" (are you trying to evaluate?)`);
  83. return this._contextPromise;
  84. }
  85. /**
  86. * @param {Function|string} pageFunction
  87. * @param {!Array<*>} args
  88. * @return {!Promise<!Puppeteer.JSHandle>}
  89. */
  90. async evaluateHandle(pageFunction, ...args) {
  91. const context = await this.executionContext();
  92. return context.evaluateHandle(pageFunction, ...args);
  93. }
  94. /**
  95. * @param {Function|string} pageFunction
  96. * @param {!Array<*>} args
  97. * @return {!Promise<*>}
  98. */
  99. async evaluate(pageFunction, ...args) {
  100. const context = await this.executionContext();
  101. return context.evaluate(pageFunction, ...args);
  102. }
  103. /**
  104. * @param {string} selector
  105. * @return {!Promise<?Puppeteer.ElementHandle>}
  106. */
  107. async $(selector) {
  108. const document = await this._document();
  109. const value = await document.$(selector);
  110. return value;
  111. }
  112. /**
  113. * @return {!Promise<!Puppeteer.ElementHandle>}
  114. */
  115. async _document() {
  116. if (this._documentPromise)
  117. return this._documentPromise;
  118. this._documentPromise = this.executionContext().then(async context => {
  119. const document = await context.evaluateHandle('document');
  120. return document.asElement();
  121. });
  122. return this._documentPromise;
  123. }
  124. /**
  125. * @param {string} expression
  126. * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
  127. */
  128. async $x(expression) {
  129. const document = await this._document();
  130. const value = await document.$x(expression);
  131. return value;
  132. }
  133. /**
  134. * @param {string} selector
  135. * @param {Function|string} pageFunction
  136. * @param {!Array<*>} args
  137. * @return {!Promise<(!Object|undefined)>}
  138. */
  139. async $eval(selector, pageFunction, ...args) {
  140. const document = await this._document();
  141. return document.$eval(selector, pageFunction, ...args);
  142. }
  143. /**
  144. * @param {string} selector
  145. * @param {Function|string} pageFunction
  146. * @param {!Array<*>} args
  147. * @return {!Promise<(!Object|undefined)>}
  148. */
  149. async $$eval(selector, pageFunction, ...args) {
  150. const document = await this._document();
  151. const value = await document.$$eval(selector, pageFunction, ...args);
  152. return value;
  153. }
  154. /**
  155. * @param {string} selector
  156. * @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
  157. */
  158. async $$(selector) {
  159. const document = await this._document();
  160. const value = await document.$$(selector);
  161. return value;
  162. }
  163. /**
  164. * @return {!Promise<String>}
  165. */
  166. async content() {
  167. return await this.evaluate(() => {
  168. let retVal = '';
  169. if (document.doctype)
  170. retVal = new XMLSerializer().serializeToString(document.doctype);
  171. if (document.documentElement)
  172. retVal += document.documentElement.outerHTML;
  173. return retVal;
  174. });
  175. }
  176. /**
  177. * @param {string} html
  178. * @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
  179. */
  180. async setContent(html, options = {}) {
  181. const {
  182. waitUntil = ['load'],
  183. timeout = this._timeoutSettings.navigationTimeout(),
  184. } = options;
  185. // We rely upon the fact that document.open() will reset frame lifecycle with "init"
  186. // lifecycle event. @see https://crrev.com/608658
  187. await this.evaluate(html => {
  188. document.open();
  189. document.write(html);
  190. document.close();
  191. }, html);
  192. const watcher = new LifecycleWatcher(this._frameManager, this._frame, waitUntil, timeout);
  193. const error = await Promise.race([
  194. watcher.timeoutOrTerminationPromise(),
  195. watcher.lifecyclePromise(),
  196. ]);
  197. watcher.dispose();
  198. if (error)
  199. throw error;
  200. }
  201. /**
  202. * @param {!{url?: string, path?: string, content?: string, type?: string}} options
  203. * @return {!Promise<!Puppeteer.ElementHandle>}
  204. */
  205. async addScriptTag(options) {
  206. const {
  207. url = null,
  208. path = null,
  209. content = null,
  210. type = ''
  211. } = options;
  212. if (url !== null) {
  213. try {
  214. const context = await this.executionContext();
  215. return (await context.evaluateHandle(addScriptUrl, url, type)).asElement();
  216. } catch (error) {
  217. throw new Error(`Loading script from ${url} failed`);
  218. }
  219. }
  220. if (path !== null) {
  221. let contents = await readFileAsync(path, 'utf8');
  222. contents += '//# sourceURL=' + path.replace(/\n/g, '');
  223. const context = await this.executionContext();
  224. return (await context.evaluateHandle(addScriptContent, contents, type)).asElement();
  225. }
  226. if (content !== null) {
  227. const context = await this.executionContext();
  228. return (await context.evaluateHandle(addScriptContent, content, type)).asElement();
  229. }
  230. throw new Error('Provide an object with a `url`, `path` or `content` property');
  231. /**
  232. * @param {string} url
  233. * @param {string} type
  234. * @return {!Promise<!HTMLElement>}
  235. */
  236. async function addScriptUrl(url, type) {
  237. const script = document.createElement('script');
  238. script.src = url;
  239. if (type)
  240. script.type = type;
  241. const promise = new Promise((res, rej) => {
  242. script.onload = res;
  243. script.onerror = rej;
  244. });
  245. document.head.appendChild(script);
  246. await promise;
  247. return script;
  248. }
  249. /**
  250. * @param {string} content
  251. * @param {string} type
  252. * @return {!HTMLElement}
  253. */
  254. function addScriptContent(content, type = 'text/javascript') {
  255. const script = document.createElement('script');
  256. script.type = type;
  257. script.text = content;
  258. let error = null;
  259. script.onerror = e => error = e;
  260. document.head.appendChild(script);
  261. if (error)
  262. throw error;
  263. return script;
  264. }
  265. }
  266. /**
  267. * @param {!{url?: string, path?: string, content?: string}} options
  268. * @return {!Promise<!Puppeteer.ElementHandle>}
  269. */
  270. async addStyleTag(options) {
  271. const {
  272. url = null,
  273. path = null,
  274. content = null
  275. } = options;
  276. if (url !== null) {
  277. try {
  278. const context = await this.executionContext();
  279. return (await context.evaluateHandle(addStyleUrl, url)).asElement();
  280. } catch (error) {
  281. throw new Error(`Loading style from ${url} failed`);
  282. }
  283. }
  284. if (path !== null) {
  285. let contents = await readFileAsync(path, 'utf8');
  286. contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
  287. const context = await this.executionContext();
  288. return (await context.evaluateHandle(addStyleContent, contents)).asElement();
  289. }
  290. if (content !== null) {
  291. const context = await this.executionContext();
  292. return (await context.evaluateHandle(addStyleContent, content)).asElement();
  293. }
  294. throw new Error('Provide an object with a `url`, `path` or `content` property');
  295. /**
  296. * @param {string} url
  297. * @return {!Promise<!HTMLElement>}
  298. */
  299. async function addStyleUrl(url) {
  300. const link = document.createElement('link');
  301. link.rel = 'stylesheet';
  302. link.href = url;
  303. const promise = new Promise((res, rej) => {
  304. link.onload = res;
  305. link.onerror = rej;
  306. });
  307. document.head.appendChild(link);
  308. await promise;
  309. return link;
  310. }
  311. /**
  312. * @param {string} content
  313. * @return {!Promise<!HTMLElement>}
  314. */
  315. async function addStyleContent(content) {
  316. const style = document.createElement('style');
  317. style.type = 'text/css';
  318. style.appendChild(document.createTextNode(content));
  319. const promise = new Promise((res, rej) => {
  320. style.onload = res;
  321. style.onerror = rej;
  322. });
  323. document.head.appendChild(style);
  324. await promise;
  325. return style;
  326. }
  327. }
  328. /**
  329. * @param {string} selector
  330. * @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
  331. */
  332. async click(selector, options) {
  333. const handle = await this.$(selector);
  334. assert(handle, 'No node found for selector: ' + selector);
  335. await handle.click(options);
  336. await handle.dispose();
  337. }
  338. /**
  339. * @param {string} selector
  340. */
  341. async focus(selector) {
  342. const handle = await this.$(selector);
  343. assert(handle, 'No node found for selector: ' + selector);
  344. await handle.focus();
  345. await handle.dispose();
  346. }
  347. /**
  348. * @param {string} selector
  349. */
  350. async hover(selector) {
  351. const handle = await this.$(selector);
  352. assert(handle, 'No node found for selector: ' + selector);
  353. await handle.hover();
  354. await handle.dispose();
  355. }
  356. /**
  357. * @param {string} selector
  358. * @param {!Array<string>} values
  359. * @return {!Promise<!Array<string>>}
  360. */
  361. async select(selector, ...values) {
  362. const handle = await this.$(selector);
  363. assert(handle, 'No node found for selector: ' + selector);
  364. const result = await handle.select(...values);
  365. await handle.dispose();
  366. return result;
  367. }
  368. /**
  369. * @param {string} selector
  370. */
  371. async tap(selector) {
  372. const handle = await this.$(selector);
  373. assert(handle, 'No node found for selector: ' + selector);
  374. await handle.tap();
  375. await handle.dispose();
  376. }
  377. /**
  378. * @param {string} selector
  379. * @param {string} text
  380. * @param {{delay: (number|undefined)}=} options
  381. */
  382. async type(selector, text, options) {
  383. const handle = await this.$(selector);
  384. assert(handle, 'No node found for selector: ' + selector);
  385. await handle.type(text, options);
  386. await handle.dispose();
  387. }
  388. /**
  389. * @param {string} selector
  390. * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
  391. * @return {!Promise<?Puppeteer.ElementHandle>}
  392. */
  393. waitForSelector(selector, options) {
  394. return this._waitForSelectorOrXPath(selector, false, options);
  395. }
  396. /**
  397. * @param {string} xpath
  398. * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
  399. * @return {!Promise<?Puppeteer.ElementHandle>}
  400. */
  401. waitForXPath(xpath, options) {
  402. return this._waitForSelectorOrXPath(xpath, true, options);
  403. }
  404. /**
  405. * @param {Function|string} pageFunction
  406. * @param {!{polling?: string|number, timeout?: number}=} options
  407. * @return {!Promise<!Puppeteer.JSHandle>}
  408. */
  409. waitForFunction(pageFunction, options = {}, ...args) {
  410. const {
  411. polling = 'raf',
  412. timeout = this._timeoutSettings.timeout(),
  413. } = options;
  414. return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
  415. }
  416. /**
  417. * @return {!Promise<string>}
  418. */
  419. async title() {
  420. return this.evaluate(() => document.title);
  421. }
  422. /**
  423. * @param {string} selectorOrXPath
  424. * @param {boolean} isXPath
  425. * @param {!{visible?: boolean, hidden?: boolean, timeout?: number}=} options
  426. * @return {!Promise<?Puppeteer.ElementHandle>}
  427. */
  428. async _waitForSelectorOrXPath(selectorOrXPath, isXPath, options = {}) {
  429. const {
  430. visible: waitForVisible = false,
  431. hidden: waitForHidden = false,
  432. timeout = this._timeoutSettings.timeout(),
  433. } = options;
  434. const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
  435. const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
  436. const waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden);
  437. const handle = await waitTask.promise;
  438. if (!handle.asElement()) {
  439. await handle.dispose();
  440. return null;
  441. }
  442. return handle.asElement();
  443. /**
  444. * @param {string} selectorOrXPath
  445. * @param {boolean} isXPath
  446. * @param {boolean} waitForVisible
  447. * @param {boolean} waitForHidden
  448. * @return {?Node|boolean}
  449. */
  450. function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {
  451. const node = isXPath
  452. ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
  453. : document.querySelector(selectorOrXPath);
  454. if (!node)
  455. return waitForHidden;
  456. if (!waitForVisible && !waitForHidden)
  457. return node;
  458. const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
  459. const style = window.getComputedStyle(element);
  460. const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
  461. const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
  462. return success ? node : null;
  463. /**
  464. * @return {boolean}
  465. */
  466. function hasVisibleBoundingBox() {
  467. const rect = element.getBoundingClientRect();
  468. return !!(rect.top || rect.bottom || rect.width || rect.height);
  469. }
  470. }
  471. }
  472. }
  473. class WaitTask {
  474. /**
  475. * @param {!DOMWorld} domWorld
  476. * @param {Function|string} predicateBody
  477. * @param {string|number} polling
  478. * @param {number} timeout
  479. * @param {!Array<*>} args
  480. */
  481. constructor(domWorld, predicateBody, title, polling, timeout, ...args) {
  482. if (helper.isString(polling))
  483. assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
  484. else if (helper.isNumber(polling))
  485. assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
  486. else
  487. throw new Error('Unknown polling options: ' + polling);
  488. this._domWorld = domWorld;
  489. this._polling = polling;
  490. this._timeout = timeout;
  491. this._predicateBody = helper.isString(predicateBody) ? 'return (' + predicateBody + ')' : 'return (' + predicateBody + ')(...args)';
  492. this._args = args;
  493. this._runCount = 0;
  494. domWorld._waitTasks.add(this);
  495. this.promise = new Promise((resolve, reject) => {
  496. this._resolve = resolve;
  497. this._reject = reject;
  498. });
  499. // Since page navigation requires us to re-install the pageScript, we should track
  500. // timeout on our end.
  501. if (timeout) {
  502. const timeoutError = new TimeoutError(`waiting for ${title} failed: timeout ${timeout}ms exceeded`);
  503. this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout);
  504. }
  505. this.rerun();
  506. }
  507. /**
  508. * @param {!Error} error
  509. */
  510. terminate(error) {
  511. this._terminated = true;
  512. this._reject(error);
  513. this._cleanup();
  514. }
  515. async rerun() {
  516. const runCount = ++this._runCount;
  517. /** @type {?Puppeteer.JSHandle} */
  518. let success = null;
  519. let error = null;
  520. try {
  521. success = await (await this._domWorld.executionContext()).evaluateHandle(waitForPredicatePageFunction, this._predicateBody, this._polling, this._timeout, ...this._args);
  522. } catch (e) {
  523. error = e;
  524. }
  525. if (this._terminated || runCount !== this._runCount) {
  526. if (success)
  527. await success.dispose();
  528. return;
  529. }
  530. // Ignore timeouts in pageScript - we track timeouts ourselves.
  531. // If the frame's execution context has already changed, `frame.evaluate` will
  532. // throw an error - ignore this predicate run altogether.
  533. if (!error && await this._domWorld.evaluate(s => !s, success).catch(e => true)) {
  534. await success.dispose();
  535. return;
  536. }
  537. // When the page is navigated, the promise is rejected.
  538. // We will try again in the new execution context.
  539. if (error && error.message.includes('Execution context was destroyed'))
  540. return;
  541. // We could have tried to evaluate in a context which was already
  542. // destroyed.
  543. if (error && error.message.includes('Cannot find context with specified id'))
  544. return;
  545. if (error)
  546. this._reject(error);
  547. else
  548. this._resolve(success);
  549. this._cleanup();
  550. }
  551. _cleanup() {
  552. clearTimeout(this._timeoutTimer);
  553. this._domWorld._waitTasks.delete(this);
  554. this._runningTask = null;
  555. }
  556. }
  557. /**
  558. * @param {string} predicateBody
  559. * @param {string} polling
  560. * @param {number} timeout
  561. * @return {!Promise<*>}
  562. */
  563. async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {
  564. const predicate = new Function('...args', predicateBody);
  565. let timedOut = false;
  566. if (timeout)
  567. setTimeout(() => timedOut = true, timeout);
  568. if (polling === 'raf')
  569. return await pollRaf();
  570. if (polling === 'mutation')
  571. return await pollMutation();
  572. if (typeof polling === 'number')
  573. return await pollInterval(polling);
  574. /**
  575. * @return {!Promise<*>}
  576. */
  577. function pollMutation() {
  578. const success = predicate.apply(null, args);
  579. if (success)
  580. return Promise.resolve(success);
  581. let fulfill;
  582. const result = new Promise(x => fulfill = x);
  583. const observer = new MutationObserver(mutations => {
  584. if (timedOut) {
  585. observer.disconnect();
  586. fulfill();
  587. }
  588. const success = predicate.apply(null, args);
  589. if (success) {
  590. observer.disconnect();
  591. fulfill(success);
  592. }
  593. });
  594. observer.observe(document, {
  595. childList: true,
  596. subtree: true,
  597. attributes: true
  598. });
  599. return result;
  600. }
  601. /**
  602. * @return {!Promise<*>}
  603. */
  604. function pollRaf() {
  605. let fulfill;
  606. const result = new Promise(x => fulfill = x);
  607. onRaf();
  608. return result;
  609. function onRaf() {
  610. if (timedOut) {
  611. fulfill();
  612. return;
  613. }
  614. const success = predicate.apply(null, args);
  615. if (success)
  616. fulfill(success);
  617. else
  618. requestAnimationFrame(onRaf);
  619. }
  620. }
  621. /**
  622. * @param {number} pollInterval
  623. * @return {!Promise<*>}
  624. */
  625. function pollInterval(pollInterval) {
  626. let fulfill;
  627. const result = new Promise(x => fulfill = x);
  628. onTimeout();
  629. return result;
  630. function onTimeout() {
  631. if (timedOut) {
  632. fulfill();
  633. return;
  634. }
  635. const success = predicate.apply(null, args);
  636. if (success)
  637. fulfill(success);
  638. else
  639. setTimeout(onTimeout, pollInterval);
  640. }
  641. }
  642. }
  643. module.exports = {DOMWorld};