diff --git a/src/phantom.h b/src/phantom.h index 8688439a..a764fb76 100644 --- a/src/phantom.h +++ b/src/phantom.h @@ -41,6 +41,7 @@ #include "system.h" class WebPage; +class CustomPage; class WebServer; class Phantom: public REPLCompletable @@ -133,6 +134,8 @@ private: QList > m_servers; Config m_config; QVariantMap m_keyMap; + + friend class CustomPage; }; #endif // PHANTOM_H diff --git a/src/webpage.cpp b/src/webpage.cpp index ba0014d6..9b41a962 100644 --- a/src/webpage.cpp +++ b/src/webpage.cpp @@ -181,9 +181,16 @@ protected: QWebPage *createWindow (WebWindowType type) { Q_UNUSED(type); + WebPage *newPage; // Create a new "raw" WebPage object - WebPage *newPage = new WebPage(m_webPage); + if (m_webPage->ownsPages()) { + newPage = new WebPage(m_webPage); + } else { + newPage = new WebPage(Phantom::instance()); + Phantom::instance()->m_pages.append(newPage); + } + // Apply default settings newPage->applySettings(Phantom::instance()->defaultPageSettings()); @@ -264,6 +271,7 @@ WebPage::WebPage(QObject *parent, const QUrl &baseUrl) : REPLCompletable(parent) , m_callbacks(NULL) , m_navigationLocked(false) + , m_ownsPages(true) { setObjectName("WebPage"); m_customWebPage = new CustomPage(this); @@ -415,7 +423,6 @@ bool WebPage::navigationLocked() return m_navigationLocked; } - void WebPage::setViewportSize(const QVariantMap &size) { int w = size.value("width").toInt(); @@ -1062,6 +1069,16 @@ QObject *WebPage::getPage(const QString &windowName) const return NULL; } +bool WebPage::ownsPages() const +{ + return m_ownsPages; +} + +void WebPage::setOwnsPages(const bool owns) +{ + m_ownsPages = owns; +} + int WebPage::framesCount() const { return m_customWebPage->currentFrame()->childFrames().count(); diff --git a/src/webpage.h b/src/webpage.h index d8a16f95..1106235a 100644 --- a/src/webpage.h +++ b/src/webpage.h @@ -64,6 +64,7 @@ class WebPage: public REPLCompletable, public QWebFrame::PrintCallback Q_PROPERTY(QString windowName READ windowName) Q_PROPERTY(QObjectList pages READ pages) Q_PROPERTY(QStringList pagesWindowName READ pagesWindowName) + Q_PROPERTY(bool ownsPages READ ownsPages WRITE setOwnsPages) Q_PROPERTY(QStringList framesName READ framesName) Q_PROPERTY(QString frameName READ frameName) Q_PROPERTY(int framesCount READ framesCount) @@ -136,6 +137,8 @@ public: * * NOTE: The ownership of this array is held by the Page: it's not adviced * to have a "long running reference" to this array, as it might change. + * NOTE: If "ownsPages()" is "false", the page will create pages but not + * hold any ownership to it. Resource management is than left to the user. * * @brief pages * @return List (JS Array) containing the Pages that this page @@ -148,6 +151,8 @@ public: * NOTE: When a page is opened with "window.open", a window * "name" might be provided as second parameter. * This provides a useful list of those. + * NOTE: If "ownsPages()" is "false", the page will create pages but not + * hold any ownership of it. Resource management is than left to the user. * * @brief pagesWindowName * @return List (JS Array) containing the 'window.name'(s) of @@ -155,6 +160,26 @@ public: */ QStringList pagesWindowName() const; + /** + * Returns "true" if it owns the pages it creates (and keeps them in "pages[]"). + * Default value is "true". Can be changed using {@link setOwnsPages()}. + * + * @brief ownsPages() + * @return "true" if it owns the pages it creates in "pages[]", "false" otherwise. + */ + bool ownsPages() const; + /** + * Set if, from now on, it should own the pages it creates in "pages[]". + * Default value is "true". + * + * NOTE: When switching from "false" to "true", only the pages created + * from that point on will be owned. It's NOT retroactive. + * + * @brief setOwnsPages + * @param owns "true" to make it own the pages it creates in "pages[]", "false" otherwise. + */ + void setOwnsPages(const bool owns); + /** * Returns the number of Child Frames inside the Current Frame. * NOTE: The Current Frame changes when focus moves (via API or JS) to a specific child frame. @@ -338,6 +363,7 @@ private: WebpageCallbacks *m_callbacks; bool m_navigationLocked; QPoint m_mousePos; + bool m_ownsPages; friend class Phantom; friend class CustomPage; diff --git a/test/fs-spec-01.js b/test/fs-spec-01.js index bdaca29e..5eaab969 100644 --- a/test/fs-spec-01.js +++ b/test/fs-spec-01.js @@ -6,11 +6,11 @@ describe("Basic Files API (read, write, remove, ...)", function() { FILENAME_ENC = FILENAME + ".enc", FILENAME_BIN = FILENAME + ".bin", ABSENT = "absent-01.test"; - + it("should be able to create and write a file", function() { try{ var f = fs.open(FILENAME, "w"); - + f.write("hello"); f.writeLine(""); f.writeLine("world"); @@ -25,31 +25,29 @@ describe("Basic Files API (read, write, remove, ...)", function() { expect(fs.exists(FILENAME_EMPTY)).toBeTruthy(); expect(fs.size(FILENAME_EMPTY)).toEqual(0); }); - + it("should be able to read content from a file", function() { var content = ""; try{ var f = fs.open(FILENAME, "r"); - - content = f.read(); + + content = f.read(); f.close(); } catch (e) { } expect(content).toEqual("hello\nworld\n"); }); - + it("should be able to read/write/append content from a file", function() { var content = ""; try{ var f = fs.open(FILENAME, "rw+"); - console.log(f.read().length); f.writeLine("asdf"); content = f.read(); - console.log(content.length); f.close(); } catch (e) { } expect(content).toEqual("hello\nworld\nasdf\n"); }); - + it("should be able to copy a file", function() { expect(fs.exists(FILENAME_COPY)).toBeFalsy(); fs.copy(FILENAME, FILENAME_COPY); @@ -77,7 +75,7 @@ describe("Basic Files API (read, write, remove, ...)", function() { fs.remove(FILENAME_COPY); expect(fs.exists(FILENAME_COPY)).toBeFalsy(); }); - + it("should be able to remove an empty file", function() { expect(fs.exists(FILENAME_EMPTY)).toBeTruthy(); fs.remove(FILENAME_EMPTY); @@ -139,4 +137,4 @@ describe("Basic Files API (read, write, remove, ...)", function() { } catch (e) { } expect(content).toEqual(output); }); -}); \ No newline at end of file +}); diff --git a/test/webpage-spec.js b/test/webpage-spec.js index 99fc0085..658abee9 100644 --- a/test/webpage-spec.js +++ b/test/webpage-spec.js @@ -157,6 +157,7 @@ describe("WebPage object", function() { expectHasFunction(page, 'loadStarted'); expectHasFunction(page, 'openUrl'); expectHasFunction(page, 'release'); + expectHasFunction(page, 'close'); expectHasFunction(page, 'render'); expectHasFunction(page, 'resourceReceived'); expectHasFunction(page, 'resourceRequested'); @@ -242,7 +243,7 @@ describe("WebPage object", function() { return page.evaluate(function() { return document.querySelector('input').value; }); - } + }; page.sendEvent('keypress', phantom.keys.A); expect(getText()).toEqual("A"); page.sendEvent('keypress', phantom.keys.B); @@ -431,7 +432,7 @@ describe("WebPage object", function() { runs(function() { page.evaluate(function() { - setTimeout(function() { referenceError }, 0); + setTimeout(function() { referenceError(); }, 0); }); }); @@ -440,13 +441,13 @@ describe("WebPage object", function() { runs(function() { expect(lastError).toEqual("ReferenceError: Can't find variable: referenceError"); - page.evaluate(function() { referenceError2 }); + page.evaluate(function() { referenceError2(); }); expect(lastError).toEqual("ReferenceError: Can't find variable: referenceError2"); - page.evaluate(function() { throw "foo" }); + page.evaluate(function() { throw "foo"; }); expect(lastError).toEqual("foo"); - page.evaluate(function() { throw Error("foo") }); + page.evaluate(function() { throw Error("foo"); }); expect(lastError).toEqual("Error: foo"); }); }); @@ -462,32 +463,32 @@ describe("WebPage object", function() { caughtError = false; try { - referenceError + referenceError(); } catch(e) { caughtError = true; } }); expect(hadError).toEqual(false); - expect(page.evaluate(function() { return caughtError })).toEqual(true); + expect(page.evaluate(function() { return caughtError; })).toEqual(true); }); - }) + }); it("reports the sourceURL and line of errors", function() { runs(function() { var e1, e2; try { - referenceError + referenceError(); } catch (e) { - e1 = e - }; + e1 = e; + } try { - referenceError + referenceError(); } catch (e) { - e2 = e - }; + e2 = e; + } expect(e1.sourceURL).toMatch(/webpage-spec.js$/); expect(e1.line).toBeGreaterThan(1); @@ -503,15 +504,15 @@ describe("WebPage object", function() { runs(function() { function test() { - ErrorHelper.foo() - }; + ErrorHelper.foo(); + } var err; try { - test() + test(); } catch (e) { - err = e - }; + err = e; + } var lines = err.stack.split("\n"); @@ -522,8 +523,8 @@ describe("WebPage object", function() { page.injectJs(helperFile); - page.onError = function(message, s) { stack = s }; - page.evaluate(function() { setTimeout(function() { ErrorHelper.foo() }, 0) }); + page.onError = function(message, s) { stack = s; }; + page.evaluate(function() { setTimeout(function() { ErrorHelper.foo(); }, 0); }); }); waits(0); @@ -531,16 +532,16 @@ describe("WebPage object", function() { runs(function() { expect(stack[0].file).toEqual("./fixtures/error-helper.js"); expect(stack[0].line).toEqual(7); - expect(stack[0].function).toEqual("bar"); + expect(stack[0]["function"]).toEqual("bar"); }); }); it("reports errors that occur in the main context", function() { var error; - phantom.onError = function(e) { error = e }; + phantom.onError = function(e) { error = e; }; runs(function() { - setTimeout(function() { zomg }, 0); + setTimeout(function() { zomg(); }, 0); }); waits(0); @@ -881,7 +882,7 @@ describe("WebPage construction with options", function () { decodedText = page.evaluate(function() { return document.getElementsByTagName('pre')[0].innerText; }); - page.release(); + page.close(); }); it("Should support text codec " + text.codec, function() { expect(decodedText.match("^" + text.reference) == text.reference).toEqual(true); @@ -1092,7 +1093,7 @@ describe("WebPage opening and closing of windows/child-pages", function(){ var yahoo = p.getPage("yahoo"); expect(yahoo).not.toBe(null); - yahoo.release(); + yahoo.close(); waitsFor(function(){ return p.pages.length === 1; @@ -1101,6 +1102,7 @@ describe("WebPage opening and closing of windows/child-pages", function(){ runs(function(){ expect(p.pages.length).toEqual(1); expect(p.pagesWindowName).toEqual(["bing"]); + p.close(); }); }); }); @@ -1117,9 +1119,15 @@ describe("WebPage closing notification/alerting", function(){ p.close(); - expect(spy).toHaveBeenCalled(); //< called - expect(spy.calls.length).toEqual(1); //< only once - expect(spy).toHaveBeenCalledWith(p); //< called passing reference to the closing page 'p' + waitsFor(function() { + return spy.calls.length === 1; + }, "after 2sec 'onClosing' had still not been invoked", 2000); + + runs(function() { + expect(spy).toHaveBeenCalled(); //< called + expect(spy.calls.length).toEqual(1); //< only once + expect(spy).toHaveBeenCalledWith(p); //< called passing reference to the closing page 'p' + }); }); it("should call 'onClosing' when a page closes on it's own", function(){ @@ -1135,8 +1143,88 @@ describe("WebPage closing notification/alerting", function(){ window.close(); }); - expect(spy).toHaveBeenCalled(); //< called - expect(spy.calls.length).toEqual(1); //< only once - expect(spy).toHaveBeenCalledWith(p); //< called passing reference to the closing page 'p' + waitsFor(function() { + return spy.calls.length === 1; + }, "after 2sec 'onClosing' had still not been invoked", 2000); + + runs(function() { + expect(spy).toHaveBeenCalled(); //< called + expect(spy.calls.length).toEqual(1); //< only once + expect(spy).toHaveBeenCalledWith(p); //< called passing reference to the closing page 'p' + }); + }); +}); + +describe("WebPage closing notification/alerting: closing propagation control", function(){ + it("should close all 4 pages if parent page is closed (default value for 'ownsPages')", function(){ + var p = require("webpage").create(), + pages, + openPagesCount = 0; + + p.onPageCreated = jasmine.createSpy("onPageCreated spy"); + + expect(p.ownsPages).toBeTruthy(); + + p.evaluate(function() { + // yeah, I know globals. YIKES! + window.w1 = window.open("http://www.google.com", "google"); + window.w2 = window.open("http://www.yahoo.com", "yahoo"); + window.w3 = window.open("http://www.bing.com", "bing"); + }); + pages = p.pages; + openPagesCount = p.pages.length + 1; + expect(p.onPageCreated).toHaveBeenCalled(); + expect(p.onPageCreated.calls.length).toEqual(3); + expect(p.pages.length).toEqual(3); + + p.onClosing = function() { --openPagesCount; }; + pages[0].onClosing = function() { --openPagesCount; }; + pages[1].onClosing = function() { --openPagesCount; }; + pages[2].onClosing = function() { --openPagesCount; }; + + p.close(); + + waitsFor(function() { + return openPagesCount === 0; + }, "after 2sec pages were still open", 2000); + + runs(function() { + expect(openPagesCount).toBe(0); + }); + }); + + it("should NOT close all 4 pages if parent page is closed, just parent itself ('ownsPages' set to false)", function(){ + var p = require("webpage").create(), + pages, + openPagesCount = 0; + p.ownsPages = false; + + p.onPageCreated = jasmine.createSpy("onPageCreated spy"); + + expect(p.ownsPages).toBeFalsy(); + + p.evaluate(function() { + // yeah, I know globals. YIKES! + window.w1 = window.open("http://www.google.com", "google"); + window.w2 = window.open("http://www.yahoo.com", "yahoo"); + window.w3 = window.open("http://www.bing.com", "bing"); + }); + pages = p.pages; + openPagesCount = 1; + expect(p.onPageCreated).toHaveBeenCalled(); + expect(p.onPageCreated.calls.length).toEqual(3); + expect(p.pages.length).toEqual(0); + + p.onClosing = function() { --openPagesCount; }; + + p.close(); + + waitsFor(function() { + return openPagesCount === 0; + }, "after 2sec pages were still open", 2000); + + runs(function() { + expect(openPagesCount).toBe(0); + }); }); });