diff --git a/chrome/content/zotero/HiddenBrowser.jsm b/chrome/content/zotero/HiddenBrowser.jsm index ae37544862..7af27d7271 100644 --- a/chrome/content/zotero/HiddenBrowser.jsm +++ b/chrome/content/zotero/HiddenBrowser.jsm @@ -244,6 +244,16 @@ class HiddenBrowser { } } } + + /** + * @param {number | false} [allowInteractiveAfter = false] Delay (in milliseconds) before resolving on 'interactive'. + * If false, documentIsReady() won't resolve until 'complete'. + * @returns {Promise} + */ + waitForDocument({ allowInteractiveAfter = false } = {}) { + return this.browsingContext.currentWindowGlobal.getActor('DocumentIsReady') + .sendQuery('waitForDocument', { allowInteractiveAfter }); + } /** * @param {String[]} props - 'characterSet', 'title', 'bodyText', 'documentHTML', 'cookie', 'channelInfo' diff --git a/chrome/content/zotero/actors/ActorManager.jsm b/chrome/content/zotero/actors/ActorManager.jsm index 1a8812e886..76f1cb01ea 100644 --- a/chrome/content/zotero/actors/ActorManager.jsm +++ b/chrome/content/zotero/actors/ActorManager.jsm @@ -86,3 +86,9 @@ ChromeUtils.registerWindowActor("MendeleyAuth", { moduleURI: "chrome://zotero/content/actors/MendeleyAuthChild.jsm" } }); + +ChromeUtils.registerWindowActor("DocumentIsReady", { + child: { + moduleURI: "chrome://zotero/content/actors/DocumentIsReadyChild.jsm" + } +}); diff --git a/chrome/content/zotero/actors/DocumentIsReadyChild.jsm b/chrome/content/zotero/actors/DocumentIsReadyChild.jsm new file mode 100644 index 0000000000..1fbada1097 --- /dev/null +++ b/chrome/content/zotero/actors/DocumentIsReadyChild.jsm @@ -0,0 +1,14 @@ +var EXPORTED_SYMBOLS = ["DocumentIsReadyChild"]; + +let { documentIsReady } = ChromeUtils.importESModule("chrome://zotero/content/actors/actorUtils.mjs"); + +class DocumentIsReadyChild extends JSWindowActorChild { + async receiveMessage({ name, data }) { + if (name !== "waitForDocument") { + return null; + } + + let { allowInteractiveAfter } = data; + await documentIsReady(this.document, { allowInteractiveAfter }); + } +} diff --git a/chrome/content/zotero/actors/MendeleyAuthChild.jsm b/chrome/content/zotero/actors/MendeleyAuthChild.jsm index 576d9cdc7a..0a863c522a 100644 --- a/chrome/content/zotero/actors/MendeleyAuthChild.jsm +++ b/chrome/content/zotero/actors/MendeleyAuthChild.jsm @@ -2,12 +2,14 @@ var EXPORTED_SYMBOLS = ["MendeleyAuthChild"]; // eslint-disable-line no-unused-vars +let { documentIsReady } = ChromeUtils.importESModule("chrome://zotero/content/actors/actorUtils.mjs"); + class MendeleyAuthChild extends JSWindowActorChild { // eslint-disable-line no-unused-vars async receiveMessage(message) { - let window = this.contentWindow; - let document = window.document; + let document = this.document; - await this.documentIsReady(); + // Wait for 'complete' + await documentIsReady(document); switch (message.name) { case "login": @@ -36,34 +38,4 @@ class MendeleyAuthChild extends JSWindowActorChild { // eslint-disable-line no-u return false; } - - // From Mozilla's ScreenshotsComponentChild.jsm - documentIsReady() { - const contentWindow = this.contentWindow; - const document = this.document; - - function readyEnough() { - return document.readyState === "complete"; - } - - if (readyEnough()) { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - function onChange(event) { - if (event.type === "pagehide") { - document.removeEventListener("readystatechange", onChange); - contentWindow.removeEventListener("pagehide", onChange); - reject(new Error("document unloaded before it was ready")); - } - else if (readyEnough()) { - document.removeEventListener("readystatechange", onChange); - contentWindow.removeEventListener("pagehide", onChange); - resolve(); - } - } - document.addEventListener("readystatechange", onChange); - contentWindow.addEventListener("pagehide", onChange, { once: true }); - }); - } } diff --git a/chrome/content/zotero/actors/PageDataChild.jsm b/chrome/content/zotero/actors/PageDataChild.jsm index 228003d509..89716d948b 100644 --- a/chrome/content/zotero/actors/PageDataChild.jsm +++ b/chrome/content/zotero/actors/PageDataChild.jsm @@ -1,5 +1,7 @@ var EXPORTED_SYMBOLS = ["PageDataChild"]; +let { documentIsReady } = ChromeUtils.importESModule("chrome://zotero/content/actors/actorUtils.mjs"); + class PageDataChild extends JSWindowActorChild { async receiveMessage(message) { // Special case for loadURI: don't wait for document to be ready, @@ -8,10 +10,10 @@ class PageDataChild extends JSWindowActorChild { return this.loadURI(message.data.uri); } - let window = this.contentWindow; - let document = window.document; + let document = this.document; - await this.documentIsReady(); + // Wait for 'interactive' or 'complete' + await documentIsReady(document, { allowInteractiveAfter: 0 }); switch (message.name) { case "characterSet": @@ -74,34 +76,4 @@ class PageDataChild extends JSWindowActorChild { return false; } } - - // From Mozilla's ScreenshotsComponentChild.jsm - documentIsReady() { - const contentWindow = this.contentWindow; - const document = this.document; - - function readyEnough() { - return document.readyState === "complete" || document.readyState === "interactive"; - } - - if (readyEnough()) { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - function onChange(event) { - if (event.type === "pagehide") { - document.removeEventListener("readystatechange", onChange); - contentWindow.removeEventListener("pagehide", onChange); - reject(new Error("document unloaded before it was ready")); - } - else if (readyEnough()) { - document.removeEventListener("readystatechange", onChange); - contentWindow.removeEventListener("pagehide", onChange); - resolve(); - } - } - document.addEventListener("readystatechange", onChange); - contentWindow.addEventListener("pagehide", onChange, { once: true }); - }); - } } diff --git a/chrome/content/zotero/actors/SingleFileChild.jsm b/chrome/content/zotero/actors/SingleFileChild.jsm index 1214f118c8..d61e8c07b2 100644 --- a/chrome/content/zotero/actors/SingleFileChild.jsm +++ b/chrome/content/zotero/actors/SingleFileChild.jsm @@ -1,11 +1,13 @@ var EXPORTED_SYMBOLS = ["SingleFileChild"]; +let { documentIsReady } = ChromeUtils.importESModule("chrome://zotero/content/actors/actorUtils.mjs"); class SingleFileChild extends JSWindowActorChild { async receiveMessage(message) { let window = this.contentWindow; - await this.documentIsReady(); + // Wait for 'complete' + await documentIsReady(this.document); if (message.name !== 'snapshot') { return null; @@ -179,34 +181,4 @@ class SingleFileChild extends JSWindowActorChild { return true; } } - - // From Mozilla's ScreenshotsComponentChild.jsm - documentIsReady() { - const contentWindow = this.contentWindow; - const document = this.document; - - function readyEnough() { - return document.readyState === "complete"; - } - - if (readyEnough()) { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - function onChange(event) { - if (event.type === "pagehide") { - document.removeEventListener("readystatechange", onChange); - contentWindow.removeEventListener("pagehide", onChange); - reject(new Error("document unloaded before it was ready")); - } - else if (readyEnough()) { - document.removeEventListener("readystatechange", onChange); - contentWindow.removeEventListener("pagehide", onChange); - resolve(); - } - } - document.addEventListener("readystatechange", onChange); - contentWindow.addEventListener("pagehide", onChange, { once: true }); - }); - } } diff --git a/chrome/content/zotero/actors/TranslationChild.jsm b/chrome/content/zotero/actors/TranslationChild.jsm index ac8b5d76cf..6f60e61828 100644 --- a/chrome/content/zotero/actors/TranslationChild.jsm +++ b/chrome/content/zotero/actors/TranslationChild.jsm @@ -1,5 +1,6 @@ var EXPORTED_SYMBOLS = ["TranslationChild"]; +let { documentIsReady } = ChromeUtils.importESModule("chrome://zotero/content/actors/actorUtils.mjs"); const TRANSLATE_SCRIPT_PATHS = [ 'src/zotero.js', @@ -43,7 +44,8 @@ class TranslationChild extends JSWindowActorChild { _sandbox = null; async receiveMessage(message) { - await this.documentIsReady(); + // Wait for 'complete', or 'interactive' after a 100ms delay + await documentIsReady(this.document, { allowInteractiveAfter: 100 }); let { name, data } = message; switch (name) { @@ -302,39 +304,6 @@ class TranslationChild extends JSWindowActorChild { return sandbox; } - // From Mozilla's ScreenshotsComponentChild.jsm - documentIsReady() { - const contentWindow = this.contentWindow; - const document = this.document; - - if (document.readyState === "complete") { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - function ready() { - document.removeEventListener("readystatechange", onChange); - contentWindow.removeEventListener("pagehide", onChange); - resolve(); - } - - function onChange(event) { - if (event.type === "pagehide") { - document.removeEventListener("readystatechange", onChange); - contentWindow.removeEventListener("pagehide", onChange); - reject(new Error("document unloaded before it was ready")); - } - else if (document.readyState === "complete") { - ready(); - } - else if (document.readyState === "interactive") { - setTimeout(ready, 100); - } - } - document.addEventListener("readystatechange", onChange); - contentWindow.addEventListener("pagehide", onChange, { once: true }); - }); - } - didDestroy() { if (this._sandbox) { Cu.nukeSandbox(this._sandbox); diff --git a/chrome/content/zotero/actors/actorUtils.mjs b/chrome/content/zotero/actors/actorUtils.mjs new file mode 100644 index 0000000000..eb0303e78e --- /dev/null +++ b/chrome/content/zotero/actors/actorUtils.mjs @@ -0,0 +1,42 @@ +import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; + +/** + * @param {Document} document + * @param {number | false} [allowInteractiveAfter] Delay (in milliseconds) before resolving on 'interactive'. + * If false, documentIsReady() won't resolve until 'complete'. + * @returns {Promise} + */ +export async function documentIsReady(document, { allowInteractiveAfter = false } = {}) { + // Adapted from Mozilla's ScreenshotsComponentChild.jsm + + function readyEnough(readyState) { + if (readyState === "interactive" && allowInteractiveAfter !== false) { + return allowInteractiveAfter > 0 + ? new Promise(resolve => setTimeout(() => resolve(true), allowInteractiveAfter)) + : true; + } + return readyState === "complete"; + } + + let contentWindow = document.defaultView; + + if (await readyEnough(document.readyState)) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + async function onChange(event) { + if (event.type === "pagehide") { + document.removeEventListener("readystatechange", onChange); + contentWindow.removeEventListener("pagehide", onChange); + reject(new Error("document unloaded before it was ready")); + } + else if (await readyEnough(document.readyState)) { + document.removeEventListener("readystatechange", onChange); + contentWindow.removeEventListener("pagehide", onChange); + resolve(); + } + } + document.addEventListener("readystatechange", onChange); + contentWindow.addEventListener("pagehide", onChange, { once: true }); + }); +} diff --git a/test/tests/HiddenBrowserTest.js b/test/tests/HiddenBrowserTest.js index 891304b11c..8b088cca39 100644 --- a/test/tests/HiddenBrowserTest.js +++ b/test/tests/HiddenBrowserTest.js @@ -22,7 +22,6 @@ describe("HiddenBrowser", function() { '/remote.png', { handle: function (request, response) { - Zotero.debug('Something loaded the image') response.setHeader('Content-Type', 'image/png', false); response.setStatusLine(null, 200, 'OK'); response.write(''); @@ -46,7 +45,7 @@ describe("HiddenBrowser", function() { let path = OS.Path.join(getTestDataDirectory().path, 'test-hidden.html'); let browser = new HiddenBrowser({ blockRemoteResources: true }); await browser.load(path); - await browser.getPageData(['characterSet', 'bodyText']); + await browser.waitForDocument(); browser.destroy(); assert.isFalse(pngRequested); }); @@ -55,7 +54,7 @@ describe("HiddenBrowser", function() { let path = OS.Path.join(getTestDataDirectory().path, 'test-hidden.html'); let browser = new HiddenBrowser({ blockRemoteResources: false }); await browser.load(path); - await browser.getPageData(['characterSet', 'bodyText']); + await browser.waitForDocument(); browser.destroy(); assert.isTrue(pngRequested); });