Commit 07816b90 07816b9024fae416c0bdfa3cc28c52d9d1233227 by Nicolas Perriault

updated support for popups

1 parent c287c823
......@@ -45,9 +45,9 @@ That's especially useful in case a given test script is abruptly interrupted lea
The whole [CapserJS test suite](https://github.com/n1k0/casperjs/tree/master/tests/) has been migrated to use this new feature.
#### Support for child pages (frames & popups)
#### Support for popups
PhantomJS 1.7 ships with support for child pages (say popups and frames). CasperJS can now wait for a child being opened and loaded to react accordingly using the new [`Casper.waitForPage()`](http://casperjs.org/api.html#casper.waitForPage) and [`Casper.withChildPage()`](http://casperjs.org/api.html#casper.withChildPage) methods:
PhantomJS 1.7 ships with support for new opened pages — aka popups. CasperJS can now wait for a popup to be opened and loaded to react accordingly using the new [`Casper.waitForPopup()`](http://casperjs.org/api.html#casper.waitForPopup) and [`Casper.withPopup()`](http://casperjs.org/api.html#casper.withPopup) methods:
```js
casper.start('http://foo.bar/').then(function() {
......@@ -55,19 +55,23 @@ casper.start('http://foo.bar/').then(function() {
this.clickLabel('Open me a popup');
});
casper.waitForPage(/popup\.html$/, function() {
this.withChildPage(this.childPages[0], function() {
this.test.assertTitle('Popup title');
});
// this will wait for the popup to be opened and loaded
casper.waitForPopup(/popup\.html$/, function() {
this.test.assertEquals(this.popups.length, 1);
});
// this will set the popup DOM as the main active one only for time the
// step closure being executed
casper.withPopup(/popup\.html$/, function() {
this.test.assertTitle('Popup title');
});
// next step will automatically revert the current page to the initial one
casper.then(function() {
this.test.assertTitle('Main page title');
});
```
**Note:** Support for this is highly experimental though, your [feedback is warmly welcome](https://github.com/n1k0/casperjs/issues/279).
#### `Casper.mouseEvent()` now uses native events for most operations
Native mouse events from PhantomJS bring a far more accurate behavior.
......
......@@ -35,7 +35,7 @@ var events = require('events');
var fs = require('fs');
var http = require('http');
var mouse = require('mouse');
var casperpage = require('page');
var popup = require('popup');
var qs = require('querystring');
var tester = require('tester');
var utils = require('utils');
......@@ -121,7 +121,6 @@ var Casper = function Casper(options) {
this.options = utils.mergeObjects(this.defaults, options);
// properties
this.checker = null;
this.childPages = new casperpage.ChildPages();
this.cli = phantom.casperArgs;
this.colorizer = this.getColorizer();
this.currentResponse = undefined;
......@@ -141,6 +140,7 @@ var Casper = function Casper(options) {
this.mouse = mouse.create(this);
this.page = null;
this.pendingWait = false;
this.popups = popup.createStack();
this.requestUrl = 'about:blank';
this.resources = [];
this.result = {
......@@ -1737,15 +1737,15 @@ Casper.prototype.waitFor = function waitFor(testFx, then, onTimeout, timeout) {
* @param Number timeout Timeout in milliseconds (optional)
* @return Casper
*/
Casper.prototype.waitForPage = function(urlPattern, then, onTimeout, timeout) {
Casper.prototype.waitForPopup = function waitForPopup(urlPattern, then, onTimeout, timeout) {
"use strict";
return this.waitFor(function() {
return this.childPages.some(function(page) {
if (utils.betterTypeOf(urlPattern) === 'regexp') {
return urlPattern.test(page.url);
}
return page.url.indexOf(urlPattern) !== -1;
});
try {
this.popups.find(urlPattern);
return true;
} catch (e) {
return false;
}
}, then, onTimeout, timeout);
};
......@@ -1866,22 +1866,25 @@ Casper.prototype.waitWhileVisible = function waitWhileVisible(selector, then, on
* Makes the provided child page as the currently active one. Note that the
* active page will be reverted when finished.
*
* @param String|RegExp|WebPage childPageInfo Target child page information
* @param String|RegExp|WebPage popup Target child page information
* @param Function then Next step function
* @return Casper
*/
Casper.prototype.withChildPage = function withChildPage(childPageInfo, then) {
Casper.prototype.withPopup = function withPopup(popupInfo, then) {
"use strict";
var childPage = this.childPages.find(childPageInfo);
if (!utils.isFunction(then)) {
throw new CasperError("withChildPage() requires a step function.");
}
// make the childPage the currently active one
this.page = childPage;
this.then(function _step() {
var popupPage = this.popups.find(popupInfo);
if (!utils.isFunction(then)) {
throw new CasperError("withPopup() requires a step function.");
}
// make the popup page the currently active one
this.page = popupPage;
});
try {
this.then(then);
} catch (e) {
// revert to main page on error
this.log("error while processing popup step: " + e, "error");
this.page = this.mainPage;
throw e;
}
......@@ -2014,17 +2017,15 @@ function createPage(casper) {
}
casper.emit('navigation.requested', url, navigationType, navigationLocked, isMainFrame);
};
page.onPageCreated = function onPageCreated(newPage) {
casper.emit('child.page.created', newPage);
newPage.onLoadFinished = function onLoadFinished() {
casper.childPages.push(newPage);
casper.emit('child.page.loaded', newPage);
page.onPageCreated = function onPageCreated(popupPage) {
casper.emit('popup.created', popupPage);
popupPage.onLoadFinished = function onLoadFinished() {
casper.popups.push(popupPage);
casper.emit('popup.loaded', popupPage);
};
newPage.onClosing = function onClosing(closedPage) {
casper.childPages = casper.childPages.filter(function(childPages) {
return childPages.id !== closedPage.id;
});
casper.emit('child.page.closed', closedPage);
popupPage.onClosing = function onClosing(closedPopup) {
casper.popups.clean(closedPopup);
casper.emit('popup.closed', closedPopup);
};
};
page.onPrompt = function onPrompt(message, value) {
......
......@@ -33,79 +33,134 @@
var utils = require('utils');
var f = utils.format;
function createStack() {
"use strict";
return new Stack();
}
exports.createStack = createStack;
/**
* Child pages container. Implements Array prototype.
* Popups container. Implements Array prototype.
*
*/
var ChildPages = function ChildPages(){};
exports.ChildPages = ChildPages;
var Stack = function Stack(){};
exports.Stack = Stack;
ChildPages.prototype = [];
Stack.prototype = [];
/**
* Finds a child page matching the provided information. Information can be:
* Cleans the stack from closed popup.
*
* @param WebPage closed Closed popup page instance
* @return Number New stack length
*/
Stack.prototype.clean = function clean(closed) {
"use strict";
var closedIndex = -1;
this.forEach(function(popup, index) {
if (closed === popup) {
closedIndex = index;
}
});
if (closedIndex > -1) {
this.splice(closedIndex, 1);
}
return this.length;
};
/**
* Finds a popup matching the provided information. Information can be:
*
* - RegExp: matching page url
* - String: strict page url value
* - WebPage: a direct WebPage instance
*
* @param Mixed pageInfo
* @param Mixed popupInfo
* @return WebPage
*/
ChildPages.prototype.find = function find(pageInfo) {
Stack.prototype.find = function find(popupInfo) {
"use strict";
var childPage, type = utils.betterTypeOf(pageInfo);
var popup, type = utils.betterTypeOf(popupInfo);
switch (type) {
case "regexp":
childPage = this.findByRegExp(pageInfo);
popup = this.findByRegExp(popupInfo);
break;
case "string":
childPage = this.findByURL(pageInfo);
popup = this.findByURL(popupInfo);
break;
case "qtruntimeobject": // WebPage
childPage = pageInfo;
if (!utils.isWebPage(childPage) || !this.some(function(activePage) {
return activePage.id === childPage.id;
popup = popupInfo;
if (!utils.isWebPage(popup) || !this.some(function(popupPage) {
if (popupInfo.id && popupPage.id) {
return popupPage.id === popup.id;
}
return popupPage.url === popup.url;
})) {
throw new CasperError("Invalid or missing child page.");
throw new CasperError("Invalid or missing popup.");
}
break;
default:
throw new CasperError(f("Invalid pageInfo type: %s.", type));
throw new CasperError(f("Invalid popupInfo type: %s.", type));
}
return childPage;
return popup;
};
/**
* Finds the first child page which url matches a given RegExp.
* Finds the first popup which url matches a given RegExp.
*
* @param RegExp regexp
* @return WebPage
*/
ChildPages.prototype.findByRegExp = function findByRegExp(regexp) {
Stack.prototype.findByRegExp = function findByRegExp(regexp) {
"use strict";
var childPage = this.filter(function(activePage) {
return regexp.test(activePage.url);
var popup = this.filter(function(popupPage) {
return regexp.test(popupPage.url);
})[0];
if (!childPage) {
throw new CasperError(f("Couldn't find child page with url matching pattern %s", regexp));
if (!popup) {
throw new CasperError(f("Couldn't find popup with url matching pattern %s", regexp));
}
return childPage;
return popup;
};
/**
* Finds the first child page matching a given url.
* Finds the first popup matching a given url.
*
* @param String url The child WebPage url
* @return WebPage
*/
ChildPages.prototype.findByURL = function findByURL(url) {
Stack.prototype.findByURL = function findByURL(string) {
"use strict";
var childPage = this.filter(function(activePage) {
return activePage.url === url;
var popup = this.filter(function(popupPage) {
return popupPage.url.indexOf(string) !== -1;
})[0];
if (!childPage) {
throw new CasperError(f("Couldn't find child page with url %s", url));
if (!popup) {
throw new CasperError(f("Couldn't find popup with url containing '%s'", string));
}
return childPage;
return popup;
};
/**
* Returns a human readable list of current active popup urls.
*
* @return Array Mapped stack.
*/
Stack.prototype.list = function list() {
"use strict";
return this.map(function(popup) {
try {
return popup.url;
} catch (e) {
return '<deleted>';
}
});
};
/**
* String representation of current instance.
*
* @return String
*/
Stack.prototype.toString = function toString() {
"use strict";
return f("[Object Stack], having %d popup(s)" % this.length);
};
......
......@@ -2,7 +2,7 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>CasperJS test child page</title>
<title>CasperJS test popup</title>
</head>
<a href="." class="close">close</a>
<body>
......
/*jshint strict:false*/
/*global CasperError casper console phantom require*/
var utils = require('utils');
var x = require('casper').selectXPath;
casper.on('child.page.created', function(page) {
this.test.pass('child.page.created event is fired');
this.test.assert(utils.isWebPage(page),
'child.page.created event callback get a child WebPage instance');
});
casper.on('child.page.loaded', function(page) {
this.test.pass('child.page.loaded event is fired');
this.test.assertEquals(page.evaluate(function() {
return document.title;
}), 'CasperJS test index',
'child.page.loaded is triggered when child page contents are actually loaded');
});
casper.on('child.page.closed', function(page) {
this.test.assertEquals(this.childPages.length, 0, 'child.page.closed event is fired');
});
casper.start('tests/site/child-page.html');
casper.waitForPage('index.html', function() {
this.test.pass('Casper.waitForPage() waits for a child page being created');
this.test.assertEquals(this.childPages.length, 1, 'A child page has been added');
var childPage = this.childPages[0];
this.test.assert(utils.isWebPage(childPage), 'A child page is a WebPage');
this.withChildPage(childPage, function() {
this.test.assertEquals(this.page.id, childPage.id,
'Casper.withChildPage() switched to child page as current active one');
this.test.assertEval(function() {
return '__utils__' in window;
}, 'Casper.withChildPage() has client utils injected');
this.test.assertTitle('CasperJS test index',
'Casper.withChildPage() can perform evaluation on the active child page');
this.test.assertExists('h1',
'Casper.withChildPage() can perform assertions on the DOM');
this.test.assertExists(x('//h1'),
'Casper.withChildPage() can perform assertions on the DOM using XPath');
});
});
casper.thenClick('.close', function() {
this.test.assertEquals(this.childPages.length, 0, 'Child page is removed when closed');
});
casper.run(function() {
this.test.done(14);
});
/*jshint strict:false*/
/*global CasperError casper console phantom require*/
var utils = require('utils');
var x = require('casper').selectXPath;
casper.on('popup.created', function(popup) {
this.test.pass('"popup.created" event is fired');
this.test.assert(utils.isWebPage(popup),
'"popup.created" event callback get a popup page instance');
});
casper.on('popup.loaded', function(popup) {
this.test.pass('"popup.loaded" event is fired');
this.test.assertEquals(popup.evaluate(function() {
return document.title;
}), 'CasperJS test index',
'"popup.loaded" is triggered when popup content is actually loaded');
});
casper.on('popup.closed', function(popup) {
this.test.assertEquals(this.popups.length, 0, '"popup.closed" event is fired');
});
casper.start('tests/site/popup.html');
casper.waitForPopup('index.html', function() {
this.test.pass('Casper.waitForPopup() waits for a popup being created');
this.test.assertEquals(this.popups.length, 1, 'A popup has been added');
this.test.assert(utils.isWebPage(this.popups[0]), 'A popup is a WebPage');
});
casper.withPopup('index.html', function() {
this.test.assertUrlMatches(/index\.html$/,
'Casper.withPopup() switched to popup as current active one');
this.test.assertEval(function() {
return '__utils__' in window;
}, 'Casper.withPopup() has client utils injected');
this.test.assertExists('h1',
'Casper.withPopup() can perform assertions on the DOM');
this.test.assertExists(x('//h1'),
'Casper.withPopup() can perform assertions on the DOM using XPath');
});
casper.then(function() {
this.test.assertUrlMatches(/popup\.html$/,
'Casper.withPopup() has reverted to main page after using the popup');
});
casper.thenClick('.close', function() {
this.test.assertEquals(this.popups.length, 0, 'Popup is removed when closed');
});
casper.thenOpen('tests/site/popup.html');
casper.waitForPopup(/index\.html$/, function() {
this.test.pass('Casper.waitForPopup() waits for a popup being created');
});
casper.withPopup(/index\.html$/, function() {
this.test.assertTitle('CasperJS test index',
'Casper.withPopup() can use a regexp to identify popup');
});
casper.thenClick('.close', function() {
this.test.assertUrlMatches(/popup\.html$/,
'Casper.withPopup() has reverted to main page after using the popup');
this.test.assertEquals(this.popups.length, 0, 'Popup is removed when closed');
});
casper.run(function() {
// removes event listeners as they've now been tested already
this.removeAllListeners('popup.created');
this.removeAllListeners('popup.loaded');
this.removeAllListeners('popup.closed');
this.test.done(23);
});
/*jshint strict:false*/
/*global CasperError casper console phantom require*/
var popup = require('popup');
var utils = require('utils');
var webpage = require('webpage');
var t = casper.test;
var stack = popup.createStack();
var page1 = webpage.create();
page1.url = 'page1.html';
stack.push(page1);
t.assertEquals(stack.length, 1);
t.assert(utils.isWebPage(stack[0]));
t.assertEquals(stack[0], page1);
t.assertEquals(stack.list().length, 1);
t.assertEquals(stack.list()[0], page1.url);
var page2 = webpage.create();
page1.url = 'page2.html';
stack.push(page2);
t.assertEquals(stack.length, 2);
t.assert(utils.isWebPage(stack[1]));
t.assertEquals(stack[1], page2);
t.assertEquals(stack.list().length, 2);
t.assertEquals(stack.list()[1], page2.url);
t.assertEquals(stack.clean(page1), 1);
t.assertEquals(stack[0], page2);
t.assertEquals(stack.list().length, 1);
t.assertEquals(stack.list()[0], page2.url);
casper.test.done();