Commit 02e4bbf7 02e4bbf7105886dc5efa135be8f34e908dca1f13 by Nicolas Perriault

decoupled form filling strategies from casper object

1 parent e042ce6d
......@@ -114,6 +114,7 @@ Last, all the casper test suites have been upgraded to use the new testing featu
- Added [`Casper#eachThen()`](http://docs.casperjs.org/en/latest/modules/casper.html#eachThen)
- merged [#427](https://github.com/n1k0/casperjs/issues/427) - Added `keepFocus` option to `Casper#sendKeys()`
- fixed [#441](https://github.com/n1k0/casperjs/issues/441) - added `--ssl-protocol` option support to the `casperjs` executable
- Added [`Casper#fillSelectors()`](http://docs.casperjs.org/en/latest/modules/casper.html#fillselectors) and [`Casper#fillXPath()`](http://docs.casperjs.org/en/latest/modules/casper.html#fillxpath)
- `cli`: Now dropping an arg or an option will be reflected in their *raw* equivalent
- `cli.get()` now supports fallback values
......
......@@ -839,7 +839,10 @@ Logs a message with an optional level in an optional space. Available levels are
**Signature:** ``fill(String selector, Object values[, Boolean submit])``
Fills the fields of a form with given values and optionally submits it.
Fills the fields of a form with given values and optionally submits it. Fields
are referenced by their ``name`` attribute.
.. versionchanged:: 1.1 To use :doc:`CSS3 or XPath selectors <../selectors>` instead, check the `fillSelectors()`_ and `fillXPath()`_ methods.
Example with this sample html form:
......@@ -883,9 +886,56 @@ A script to fill and submit this form::
.. warning::
1. The ``fill()`` method currently can't fill **file fields using XPath selectors**; PhantomJS natively only allows the use of CSS3 selectors in its uploadFile method, hence this limitation.
1. The ``fill()`` method currently can't fill **file fields using XPath selectors**; PhantomJS natively only allows the use of CSS3 selectors in its ``uploadFile()`` method, hence this limitation.
2. Please Don't use CasperJS nor PhantomJS to send spam, or I'll be calling the Chuck. More seriously, please just don't.
``fillSelectors()``
-------------------------------------------------------------------------------
**Signature:** ``fillSelectors(String selector, Object values[, Boolean submit])``
.. versionadded:: 1.1
Fills form fields with given values and optionally submits it. Fields
are referenced by ``CSS3`` selectors::
casper.start('http://some.tld/contact.form', function() {
this.fill('form#contact-form', {
'input[name="subject"]': 'I am watching you',
'input[name="content"]': 'So be careful.',
'input[name="civility"]': 'Mr',
'input[name="name"]': 'Chuck Norris',
'input[name="email"]': 'chuck@norris.com',
'input[name="cc"]': true,
'input[name="attachment"]': '/Users/chuck/roundhousekick.doc'
}, true);
});
``fillXPath()``
-------------------------------------------------------------------------------
**Signature:** ``fillXPath(String selector, Object values[, Boolean submit])``
.. versionadded:: 1.1
Fills form fields with given values and optionally submits it. While the ``form`` element is always referenced by a CSS3 selector, fields are referenced by ``XPath`` selectors::
casper.start('http://some.tld/contact.form', function() {
this.fill('form#contact-form', {
'//input[@name="subject"]': 'I am watching you',
'//input[@name="content"]': 'So be careful.',
'//input[@name="civility"]': 'Mr',
'//input[@name="name"]': 'Chuck Norris',
'//input[@name="email"]': 'chuck@norris.com',
'//input[@name="cc"]': true,
}, true);
});
.. warning::
The ``fillXPath()`` method currently can't fill **file fields using XPath selectors**; PhantomJS natively only allows the use of CSS3 selectors in its ``uploadFile()`` method, hence this limitation.
.. index:: URL
``getCurrentUrl()``
......@@ -1905,6 +1955,7 @@ is changed to a different value before processing the next step. Uses `waitFor()
-------------------------------------------------------------------------------
**Signature:** ``waitForText(String text[, Function then, Function onTimeout, Number timeout])``
.. versionadded:: 1.0
Waits until the passed text is present in the page contents before processing the immediate next step. Uses `waitFor()`_::
......
......@@ -752,41 +752,51 @@ Casper.prototype.fetchText = function fetchText(selector) {
*/
Casper.prototype.fillForm = function fillForm(selector, vals, options) {
"use strict";
var submit, selectorFunction;
this.checkStarted();
selectorFunction = options && options.selectorFunction;
submit = options.submit === true ? options.submit : false;
var selectorType = options && options.selectorType || "names",
submit = !!(options && options.submit);
this.emit('fill', selector, vals, options);
var fillResults = this.evaluate(function _evaluate(selector, vals, selectorFunction) {
return __utils__.fill(selector, vals, selectorFunction);
}, selector, vals, selectorFunction);
var fillResults = this.evaluate(function _evaluate(selector, vals, selectorType) {
try {
return __utils__.fill(selector, vals, selectorType);
} catch (exception) {
return {exception: exception.toString()};
}
}, selector, vals, selectorType);
if (!fillResults) {
throw new CasperError("Unable to fill form");
} else if (fillResults && fillResults.exception) {
throw new CasperError("Unable to fill form: " + fillResults.exception);
} else if (fillResults.errors.length > 0) {
throw new CasperError(f('Errors encountered while filling form: %s',
fillResults.errors.join('; ')));
}
// File uploads
if (fillResults.files && fillResults.files.length > 0) {
if (utils.isObject(selector) && selector.type === 'xpath') {
this.warn('Filling file upload fields is currently not supported using ' +
'XPath selectors; Please use a CSS selector instead.');
} else {
(function _each(self) {
fillResults.files.forEach(function _forEach(file) {
if (!file || !file.path) {
return;
}
if (!fs.exists(file.path)) {
throw new CasperError('Cannot upload nonexistent file: ' + file.path);
}
var fileFieldSelector = selectorFunction.call(this, file.name, selector).fullSelector;
self.page.uploadFile(fileFieldSelector, file.path);
});
})(this);
fillResults.files.forEach(function _forEach(file) {
if (!file || !file.path) {
return;
}
if (!fs.exists(file.path)) {
throw new CasperError('Cannot upload nonexistent file: ' + file.path);
}
var fileFieldSelector;
if (file.type === "names") {
fileFieldSelector = [selector, 'input[name="' + file.selector + '"]'].join(' ');
} else if (file.type === "css") {
fileFieldSelector = [selector, file.selector].join(' ');
}
this.page.uploadFile(fileFieldSelector, file.path);
}.bind(this));
}
}
// Form submission?
......@@ -817,47 +827,52 @@ Casper.prototype.fillForm = function fillForm(selector, vals, options) {
*
* @param String formSelector A DOM CSS3/XPath selector to the target form to fill
* @param Object vals Field values
* @param Boolean submit Submit the form?
* @param Boolean submit Submit the form?
*/
Casper.prototype.fillNames = function fillNames(formSelector, vals, submit) {
"use strict";
return this.fillForm(formSelector, vals, {
submit: submit,
selectorFunction: function _nameSelector(elementName, formSelector) {
return {
fullSelector: [formSelector, '[name="' + elementName + '"]'].join(' '),
elts: (this.findAll ? this.findAll('[name="' + elementName + '"]', formSelector) : null)
};
}
selectorType: 'names'
});
};
/**
* Fills a form with provided field values using the Name attribute.
* Fills a form with provided field values using CSS3 selectors.
*
* @param String formSelector A DOM CSS3/XPath selector to the target form to fill
* @param Object vals Field values
* @param Boolean submit Submit the form?
*/
Casper.prototype.fillSelectors = function fillSelectors(formSelector, vals, submit) {
"use strict";
return this.fillForm(formSelector, vals, {
submit: submit,
selectorType: 'css'
});
};
/**
* Fills a form with provided field values using the Name attribute by default.
*
* @param String formSelector A DOM CSS3/XPath selector to the target form to fill
* @param Object vals Field values
* @param Boolean submit Submit the form?
* @param Boolean submit Submit the form?
*/
Casper.prototype.fill = Casper.prototype.fillNames
Casper.prototype.fill = Casper.prototype.fillNames;
/**
* Fills a form with provided field values using CSS3 selectors.
* Fills a form with provided field values using XPath selectors.
*
* @param String formSelector A DOM CSS3/XPath selector to the target form to fill
* @param Object vals Field values
* @param Boolean submit Submit the form?
* @param Boolean submit Submit the form?
*/
Casper.prototype.fillSelectors = function fillSelectors(formSelector, vals, submit) {
Casper.prototype.fillXPath = function fillXPath(formSelector, vals, submit) {
"use strict";
return this.fillForm(formSelector, vals, {
submit: submit,
selectorFunction: function _css3Selector(inputSelector, formSelector) {
return {
fullSelector: [formSelector, inputSelector].join(' '),
elts: (this.findAll ? this.findAll(inputSelector, formSelector) : null)
};
}
selectorType: 'xpath'
});
};
......
......@@ -196,12 +196,12 @@
/**
* Fills a form with provided field values, and optionally submits it.
*
* @param HTMLElement|String form A form element, or a CSS3 selector to a form element
* @param Object vals Field values
* @param Function findFunction A function to be used for getting the selector for the element or a list of matching elements (optional)
* @return Object An object containing setting result for each field, including file uploads
* @param HTMLElement|String form A form element, or a CSS3 selector to a form element
* @param Object vals Field values
* @param Function findType Element finder type (css, names, xpath)
* @return Object An object containing setting result for each field, including file uploads
*/
this.fill = function fill(form, vals, findFunction) {
this.fill = function fill(form, vals, findType) {
/*jshint maxcomplexity:8*/
var out = {
errors: [],
......@@ -209,13 +209,6 @@
files: []
};
findFunction = findFunction || function _nameSelector(elementName, formSelector) {
return {
fullSelector: [formSelector, '[name="' + elementName + '"]'].join(' '),
elts: this.findAll('[name="' + elementName + '"]', formSelector)
};
};
if (!(form instanceof HTMLElement) || typeof form === "string") {
this.log("attempting to fetch form element from selector: '" + form + "'", "info");
try {
......@@ -227,30 +220,45 @@
}
}
}
if (!form) {
out.errors.push("form not found");
return out;
}
for (var name in vals) {
if (!vals.hasOwnProperty(name)) {
var finders = {
css: function(inputSelector, formSelector) {
return this.findAll(inputSelector, form);
},
names: function(elementName, formSelector) {
return this.findAll('[name="' + elementName + '"]', form);
},
xpath: function(xpath, formSelector) {
return this.findAll({type: "xpath", path: xpath}, form);
}
};
for (var fieldSelector in vals) {
if (!vals.hasOwnProperty(fieldSelector)) {
continue;
}
var field = findFunction.call(this, name, form).elts;
var value = vals[name];
var field = finders[findType || "names"].call(this, fieldSelector, form),
value = vals[fieldSelector];
if (!field || field.length === 0) {
out.errors.push('no field named "' + name + '" in form');
out.errors.push('no field matching ' + findType + ' selector "' + fieldSelector + '" in form');
continue;
}
try {
out.fields[name] = this.setField(field, value);
out.fields[fieldSelector] = this.setField(field, value);
} catch (err) {
if (err.name === "FileUploadError") {
out.files.push({
name: name,
type: findType,
selector: fieldSelector,
path: err.path
});
} else if(err.name === "FieldNotFound") {
out.errors.push('Form field named "' + name + '" was not found.');
} else if (err.name === "FieldNotFound") {
out.errors.push('Unable to find field element in form: ' + err.toString());
} else {
out.errors.push(err.toString());
}
......@@ -680,26 +688,33 @@
/*jshint maxcomplexity:99 */
var logValue, fields, out;
value = logValue = (value || "");
if (field instanceof NodeList) {
if (field instanceof NodeList || field instanceof Array) {
fields = field;
field = fields[0];
}
if (!(field instanceof HTMLElement)) {
var error = new Error('Invalid field type; only HTMLElement and NodeList are supported');
error.name = 'FieldNotFound';
throw error;
}
if (this.options && this.options.safeLogs && field.getAttribute('type') === "password") {
// obfuscate password value
logValue = new Array(value.length + 1).join("*");
}
this.log('Set "' + field.getAttribute('name') + '" field value to ' + logValue, "debug");
try {
field.focus();
} catch (e) {
this.log("Unable to focus() input field " + field.getAttribute('name') + ": " + e, "warning");
}
var nodeName = field.nodeName.toLowerCase();
switch (nodeName) {
case "input":
var type = field.getAttribute('type') || "text";
......
......@@ -2,7 +2,40 @@
/*jshint strict:false*/
var fs = require('fs');
casper.test.begin('fill() tests', 15, function(test) {
function testFormValues(test) {
test.assertField('email', 'chuck@norris.com',
'can fill an input[type=text] form field');
test.assertField('password', 'chuck',
'can fill an input[type=password] form field')
test.assertField('content', 'Am watching thou',
'can fill a textarea form field');
test.assertField('topic', 'bar',
'can pick a value from a select form field');
test.assertField('check', true,
'can check a form checkbox');
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="choice"][value="no"]').checked;
}, true, 'can check a form radio button 1/2');
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="choice"][value="yes"]').checked;
}, false, 'can check a form radio button 2/2');
test.assertEvalEquals(function() {
return (__utils__.findOne('input[name="checklist[]"][value="1"]').checked &&
!__utils__.findOne('input[name="checklist[]"][value="2"]').checked &&
__utils__.findOne('input[name="checklist[]"][value="3"]').checked);
}, true, 'can fill a list of checkboxes');
}
function testUrl(test) {
test.assertUrlMatch(/email=chuck@norris.com/, 'input[type=email] field was submitted');
test.assertUrlMatch(/password=chuck/, 'input[type=password] field was submitted');
test.assertUrlMatch(/content=Am\+watching\+thou/, 'textarea field was submitted');
test.assertUrlMatch(/check=on/, 'input[type=checkbox] field was submitted');
test.assertUrlMatch(/choice=no/, 'input[type=radio] field was submitted');
test.assertUrlMatch(/topic=bar/, 'select field was submitted');
}
casper.test.begin('fill() & fillNames() tests', 15, function(test) {
var fpath = fs.pathJoin(phantom.casperPath, 'README.md');
casper.start('tests/site/form.html', function() {
......@@ -16,101 +49,62 @@ casper.test.begin('fill() tests', 15, function(test) {
file: fpath,
'checklist[]': ['1', '3']
});
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="email"]').value;
}, 'chuck@norris.com', 'Casper.fill() can fill an input[type=text] form field');
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="password"]').value;
}, 'chuck', 'Casper.fill() can fill an input[type=password] form field');
test.assertEvalEquals(function() {
return __utils__.findOne('textarea[name="content"]').value;
}, 'Am watching thou', 'Casper.fill() can fill a textarea form field');
test.assertEvalEquals(function() {
return __utils__.findOne('select[name="topic"]').value;
}, 'bar', 'Casper.fill() can pick a value from a select form field');
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="check"]').checked;
}, true, 'Casper.fill() can check a form checkbox');
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="choice"][value="no"]').checked;
}, true, 'Casper.fill() can check a form radio button 1/2');
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="choice"][value="yes"]').checked;
}, false, 'Casper.fill() can check a form radio button 2/2');
testFormValues(test);
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="file"]').files.length === 1;
}, true, 'Casper.fill() can select a file to upload');
test.assertEvalEquals(function() {
return (__utils__.findOne('input[name="checklist[]"][value="1"]').checked &&
!__utils__.findOne('input[name="checklist[]"][value="2"]').checked &&
__utils__.findOne('input[name="checklist[]"][value="3"]').checked);
}, true, 'Casper.fill() can fill a list of checkboxes');
}, true, 'can select a file to upload');
});
casper.thenClick('input[type="submit"]', function() {
test.assertUrlMatch(/email=chuck@norris.com/, 'Casper.fill() input[type=email] field was submitted');
test.assertUrlMatch(/password=chuck/, 'Casper.fill() input[type=password] field was submitted');
test.assertUrlMatch(/content=Am\+watching\+thou/, 'Casper.fill() textarea field was submitted');
test.assertUrlMatch(/check=on/, 'Casper.fill() input[type=checkbox] field was submitted');
test.assertUrlMatch(/choice=no/, 'Casper.fill() input[type=radio] field was submitted');
test.assertUrlMatch(/topic=bar/, 'Casper.fill() select field was submitted');
testUrl(test);
});
casper.run(function() {
test.done();
});
});
casper.test.begin('fillSelector() tests', 15, function(test) {
casper.test.begin('fillSelectors() tests', 15, function(test) {
var fpath = fs.pathJoin(phantom.casperPath, 'README.md');
casper.start('tests/site/form.html', function() {
this.fillSelectors('form[action="result.html"]', {
"input[name='email']": 'chuck@norris.com',
"input[name='password']": 'chuck',
"textarea[name='content']": 'Am watching thou',
"input[name='check']": true,
"input[name='choice']": 'no',
"select[name='topic']": 'bar',
"input[name='file']": fpath,
"input[name='checklist[]']": ['1', '3']
}
);
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="email"]').value;
}, 'chuck@norris.com', 'Casper.fill() can fill an input[type=text] form field');
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="password"]').value;
}, 'chuck', 'Casper.fill() can fill an input[type=password] form field');
test.assertEvalEquals(function() {
return __utils__.findOne('textarea[name="content"]').value;
}, 'Am watching thou', 'Casper.fill() can fill a textarea form field');
test.assertEvalEquals(function() {
return __utils__.findOne('select[name="topic"]').value;
}, 'bar', 'Casper.fill() can pick a value from a select form field');
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="check"]').checked;
}, true, 'Casper.fill() can check a form checkbox');
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="choice"][value="no"]').checked;
}, true, 'Casper.fill() can check a form radio button 1/2');
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="choice"][value="yes"]').checked;
}, false, 'Casper.fill() can check a form radio button 2/2');
"input[name='email']": 'chuck@norris.com',
"input[name='password']": 'chuck',
"textarea[name='content']": 'Am watching thou',
"input[name='check']": true,
"input[name='choice']": 'no',
"select[name='topic']": 'bar',
"input[name='file']": fpath,
"input[name='checklist[]']": ['1', '3']
});
testFormValues(test);
test.assertEvalEquals(function() {
return __utils__.findOne('input[name="file"]').files.length === 1;
}, true, 'Casper.fill() can select a file to upload');
test.assertEvalEquals(function() {
return (__utils__.findOne('input[name="checklist[]"][value="1"]').checked &&
!__utils__.findOne('input[name="checklist[]"][value="2"]').checked &&
__utils__.findOne('input[name="checklist[]"][value="3"]').checked);
}, true, 'Casper.fill() can fill a list of checkboxes');
}, true, 'can select a file to upload');
});
casper.thenClick('input[type="submit"]', function() {
testUrl(test);
});
casper.run(function() {
test.done();
});
});
casper.test.begin('fillXPath() tests', 14, function(test) {
casper.start('tests/site/form.html', function() {
this.fillXPath('form[action="result.html"]', {
'//input[@name="email"]': 'chuck@norris.com',
'//input[@name="password"]': 'chuck',
'//textarea[@name="content"]': 'Am watching thou',
'//input[@name="check"]': true,
'//input[@name="choice"]': 'no',
'//select[@name="topic"]': 'bar',
'//input[@name="checklist[]"]': ['1', '3']
});
testFormValues(test);
// note: file inputs cannot be filled using XPath
});
casper.thenClick('input[type="submit"]', function() {
test.assertUrlMatch(/email=chuck@norris.com/, 'Casper.fill() input[type=email] field was submitted');
test.assertUrlMatch(/password=chuck/, 'Casper.fill() input[type=password] field was submitted');
test.assertUrlMatch(/content=Am\+watching\+thou/, 'Casper.fill() textarea field was submitted');
test.assertUrlMatch(/check=on/, 'Casper.fill() input[type=checkbox] field was submitted');
test.assertUrlMatch(/choice=no/, 'Casper.fill() input[type=radio] field was submitted');
test.assertUrlMatch(/topic=bar/, 'Casper.fill() select field was submitted');
testUrl(test);
});
casper.run(function() {
test.done();
......