LifecycleWatcher.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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 {helper, assert} = require('./helper');
  17. const {Events} = require('./Events');
  18. const {TimeoutError} = require('./Errors');
  19. class LifecycleWatcher {
  20. /**
  21. * @param {!Puppeteer.FrameManager} frameManager
  22. * @param {!Puppeteer.Frame} frame
  23. * @param {string|!Array<string>} waitUntil
  24. * @param {number} timeout
  25. */
  26. constructor(frameManager, frame, waitUntil, timeout) {
  27. if (Array.isArray(waitUntil))
  28. waitUntil = waitUntil.slice();
  29. else if (typeof waitUntil === 'string')
  30. waitUntil = [waitUntil];
  31. this._expectedLifecycle = waitUntil.map(value => {
  32. const protocolEvent = puppeteerToProtocolLifecycle.get(value);
  33. assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
  34. return protocolEvent;
  35. });
  36. this._frameManager = frameManager;
  37. this._frame = frame;
  38. this._initialLoaderId = frame._loaderId;
  39. this._timeout = timeout;
  40. /** @type {?Puppeteer.Request} */
  41. this._navigationRequest = null;
  42. this._eventListeners = [
  43. helper.addEventListener(frameManager._client, Events.CDPSession.Disconnected, () => this._terminate(new Error('Navigation failed because browser has disconnected!'))),
  44. helper.addEventListener(this._frameManager, Events.FrameManager.LifecycleEvent, this._checkLifecycleComplete.bind(this)),
  45. helper.addEventListener(this._frameManager, Events.FrameManager.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this)),
  46. helper.addEventListener(this._frameManager, Events.FrameManager.FrameDetached, this._onFrameDetached.bind(this)),
  47. helper.addEventListener(this._frameManager.networkManager(), Events.NetworkManager.Request, this._onRequest.bind(this)),
  48. ];
  49. this._sameDocumentNavigationPromise = new Promise(fulfill => {
  50. this._sameDocumentNavigationCompleteCallback = fulfill;
  51. });
  52. this._lifecyclePromise = new Promise(fulfill => {
  53. this._lifecycleCallback = fulfill;
  54. });
  55. this._newDocumentNavigationPromise = new Promise(fulfill => {
  56. this._newDocumentNavigationCompleteCallback = fulfill;
  57. });
  58. this._timeoutPromise = this._createTimeoutPromise();
  59. this._terminationPromise = new Promise(fulfill => {
  60. this._terminationCallback = fulfill;
  61. });
  62. this._checkLifecycleComplete();
  63. }
  64. /**
  65. * @param {!Puppeteer.Request} request
  66. */
  67. _onRequest(request) {
  68. if (request.frame() !== this._frame || !request.isNavigationRequest())
  69. return;
  70. this._navigationRequest = request;
  71. }
  72. /**
  73. * @param {!Puppeteer.Frame} frame
  74. */
  75. _onFrameDetached(frame) {
  76. if (this._frame === frame) {
  77. this._terminationCallback.call(null, new Error('Navigating frame was detached'));
  78. return;
  79. }
  80. this._checkLifecycleComplete();
  81. }
  82. /**
  83. * @return {?Puppeteer.Response}
  84. */
  85. navigationResponse() {
  86. return this._navigationRequest ? this._navigationRequest.response() : null;
  87. }
  88. /**
  89. * @param {!Error} error
  90. */
  91. _terminate(error) {
  92. this._terminationCallback.call(null, error);
  93. }
  94. /**
  95. * @return {!Promise<?Error>}
  96. */
  97. sameDocumentNavigationPromise() {
  98. return this._sameDocumentNavigationPromise;
  99. }
  100. /**
  101. * @return {!Promise<?Error>}
  102. */
  103. newDocumentNavigationPromise() {
  104. return this._newDocumentNavigationPromise;
  105. }
  106. /**
  107. * @return {!Promise}
  108. */
  109. lifecyclePromise() {
  110. return this._lifecyclePromise;
  111. }
  112. /**
  113. * @return {!Promise<?Error>}
  114. */
  115. timeoutOrTerminationPromise() {
  116. return Promise.race([this._timeoutPromise, this._terminationPromise]);
  117. }
  118. /**
  119. * @return {!Promise<?Error>}
  120. */
  121. _createTimeoutPromise() {
  122. if (!this._timeout)
  123. return new Promise(() => {});
  124. const errorMessage = 'Navigation timeout of ' + this._timeout + ' ms exceeded';
  125. return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, this._timeout))
  126. .then(() => new TimeoutError(errorMessage));
  127. }
  128. /**
  129. * @param {!Puppeteer.Frame} frame
  130. */
  131. _navigatedWithinDocument(frame) {
  132. if (frame !== this._frame)
  133. return;
  134. this._hasSameDocumentNavigation = true;
  135. this._checkLifecycleComplete();
  136. }
  137. _checkLifecycleComplete() {
  138. // We expect navigation to commit.
  139. if (!checkLifecycle(this._frame, this._expectedLifecycle))
  140. return;
  141. this._lifecycleCallback();
  142. if (this._frame._loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation)
  143. return;
  144. if (this._hasSameDocumentNavigation)
  145. this._sameDocumentNavigationCompleteCallback();
  146. if (this._frame._loaderId !== this._initialLoaderId)
  147. this._newDocumentNavigationCompleteCallback();
  148. /**
  149. * @param {!Puppeteer.Frame} frame
  150. * @param {!Array<string>} expectedLifecycle
  151. * @return {boolean}
  152. */
  153. function checkLifecycle(frame, expectedLifecycle) {
  154. for (const event of expectedLifecycle) {
  155. if (!frame._lifecycleEvents.has(event))
  156. return false;
  157. }
  158. for (const child of frame.childFrames()) {
  159. if (!checkLifecycle(child, expectedLifecycle))
  160. return false;
  161. }
  162. return true;
  163. }
  164. }
  165. dispose() {
  166. helper.removeEventListeners(this._eventListeners);
  167. clearTimeout(this._maximumTimer);
  168. }
  169. }
  170. const puppeteerToProtocolLifecycle = new Map([
  171. ['load', 'load'],
  172. ['domcontentloaded', 'DOMContentLoaded'],
  173. ['networkidle0', 'networkIdle'],
  174. ['networkidle2', 'networkAlmostIdle'],
  175. ]);
  176. module.exports = {LifecycleWatcher};