Update validator to 3.4.0

Closes #1379

- Convert to new api usage for both server-side and client-side
- Provide way require a negative response for boolean methods in
  default-settings.json
- Add field validation functional tests
  - Settings (General)
    - Title length validation
    - Description length validation
    - postsPerPage, numeric, min, max
  - Settings (User)
    - Bio Length validation
    - Location length validation
    - Url validation
  - Login
    - Email validation
  - Editor
    - Title required validation
This commit is contained in:
Jonathan Johnson 2014-02-27 23:51:52 -07:00
parent 8d3a54527b
commit e4bb6d08cc
13 changed files with 692 additions and 1073 deletions

View File

@ -15,6 +15,6 @@
"nprogress": "0.1.2",
"fastclick": "1.0.0",
"Countable": "2.0.2",
"validator-js": "1.5.1"
"validator-js": "3.4.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
/*globals window, $, _, Backbone, Validator */
/*globals window, $, _, Backbone, validator */
(function () {
'use strict';
@ -17,7 +17,6 @@
Views : {},
Collections : {},
Models : {},
Validate : new Validator(),
paths: ghostPaths(),
@ -62,21 +61,16 @@
});
};
Ghost.Validate.error = function (object) {
this._errors.push(object);
return this;
};
Ghost.Validate.handleErrors = function () {
validator.handleErrors = function (errors) {
Ghost.notifications.clearEverything();
_.each(Ghost.Validate._errors, function (errorObj) {
_.each(errors, function (errorObj) {
Ghost.notifications.addItem({
type: 'error',
message: errorObj.message || errorObj,
status: 'passive'
});
if (errorObj.hasOwnProperty('el')) {
errorObj.el.addClass('input-error');
}

View File

@ -1,4 +1,4 @@
/*global window, Ghost, $ */
/*global window, Ghost, $, validator */
(function () {
"use strict";
@ -25,14 +25,19 @@
event.preventDefault();
var email = this.$el.find('.email').val(),
password = this.$el.find('.password').val(),
redirect = Ghost.Views.Utils.getUrlVariables().r;
redirect = Ghost.Views.Utils.getUrlVariables().r,
validationErrors = [];
Ghost.Validate._errors = [];
Ghost.Validate.check(email).isEmail();
Ghost.Validate.check(password, "Please enter a password").len(0);
if (!validator.isEmail(email)) {
validationErrors.push("Invalid Email");
}
if (Ghost.Validate._errors.length > 0) {
Ghost.Validate.handleErrors();
if (!validator.isLength(password, 0)) {
validationErrors.push("Please enter a password");
}
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
$.ajax({
url: Ghost.paths.subdir + '/ghost/signin/',
@ -88,18 +93,27 @@
event.preventDefault();
var name = this.$('.name').val(),
email = this.$('.email').val(),
password = this.$('.password').val();
password = this.$('.password').val(),
validationErrors = [];
// This is needed due to how error handling is done. If this is not here, there will not be a time
// when there is no error.
Ghost.Validate._errors = [];
Ghost.Validate.check(name, "Please enter a name").len(1);
Ghost.Validate.check(email, "Please enter a correct email address").isEmail();
Ghost.Validate.check(password, "Your password is not long enough. It must be at least 8 characters long.").len(8);
Ghost.Validate.check(this.submitted, "Ghost is signing you up. Please wait...").equals("no");
if (!validator.isLength(name, 1)) {
validationErrors.push("Please enter a name.");
}
if (Ghost.Validate._errors.length > 0) {
Ghost.Validate.handleErrors();
if (!validator.isEmail(email)) {
validationErrors.push("Please enter a correct email address.");
}
if (!validator.isLength(password, 0)) {
validationErrors.push("Please enter a password");
}
if (!validator.equals(this.submitted, "no")) {
validationErrors.push("Ghost is signing you up. Please wait...");
}
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
this.submitted = "yes";
$.ajax({
@ -152,13 +166,15 @@
submitHandler: function (event) {
event.preventDefault();
var email = this.$el.find('.email').val();
var email = this.$el.find('.email').val(),
validationErrors = [];
Ghost.Validate._errors = [];
Ghost.Validate.check(email).isEmail();
if (!validator.isEmail(email)) {
validationErrors.push("Please enter a correct email address.");
}
if (Ghost.Validate._errors.length > 0) {
Ghost.Validate.handleErrors();
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
$.ajax({
url: Ghost.paths.subdir + '/ghost/forgotten/',

View File

@ -1,4 +1,4 @@
/*global document, Ghost, $, _, Countable */
/*global document, Ghost, $, _, Countable, validator */
(function () {
"use strict";
@ -160,28 +160,32 @@
description = this.$('#blog-description').val(),
email = this.$('#email-address').val(),
postsPerPage = this.$('#postsPerPage').val(),
permalinks = this.$('#permalinks').is(':checked') ? '/:year/:month/:day/:slug/' : '/:slug/';
permalinks = this.$('#permalinks').is(':checked') ? '/:year/:month/:day/:slug/' : '/:slug/',
validationErrors = [];
Ghost.Validate._errors = [];
Ghost.Validate
.check(title, {message: "Title is too long", el: $('#blog-title')})
.len(0, 150);
Ghost.Validate
.check(description, {message: "Description is too long", el: $('#blog-description')})
.len(0, 200);
Ghost.Validate
.check(email, {message: "Please supply a valid email address", el: $('#email-address')})
.isEmail().len(0, 254);
Ghost.Validate
.check(postsPerPage, {message: "Please use a number less than 1000", el: $('postsPerPage')})
.isInt().max(1000);
Ghost.Validate
.check(postsPerPage, {message: "Please use a number greater than 0", el: $('postsPerPage')})
.isInt().min(0);
if (!validator.isLength(title, 0, 150)) {
validationErrors.push({message: "Title is too long", el: $('#blog-title')});
}
if (!validator.isLength(description, 0, 200)) {
validationErrors.push({message: "Description is too long", el: $('#blog-description')});
}
if (!validator.isEmail(email) || !validator.isLength(email, 0, 254)) {
validationErrors.push({message: "Please supply a valid email address", el: $('#email-address')});
}
if (!validator.isInt(postsPerPage) || postsPerPage > 1000) {
validationErrors.push({message: "Please use a number less than 1000", el: $('postsPerPage')});
}
if (!validator.isInt(postsPerPage) || postsPerPage < 0) {
validationErrors.push({message: "Please use a number greater than 0", el: $('postsPerPage')});
}
if (Ghost.Validate._errors.length > 0) {
Ghost.Validate.handleErrors();
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
this.model.save({
title: title,
@ -343,30 +347,33 @@
userEmail = this.$('#user-email').val(),
userLocation = this.$('#user-location').val(),
userWebsite = this.$('#user-website').val(),
userBio = this.$('#user-bio').val();
userBio = this.$('#user-bio').val(),
validationErrors = [];
Ghost.Validate._errors = [];
Ghost.Validate
.check(userName, {message: "Name is too long", el: $('#user-name')})
.len(0, 150);
Ghost.Validate
.check(userBio, {message: "Bio is too long", el: $('#user-bio')})
.len(0, 200);
Ghost.Validate
.check(userEmail, {message: "Please supply a valid email address", el: $('#user-email')})
.isEmail();
Ghost.Validate
.check(userLocation, {message: "Location is too long", el: $('#user-location')})
.len(0, 150);
if (userWebsite.length > 0) {
Ghost.Validate
.check(userWebsite, {message: "Please use a valid url", el: $('#user-website')})
.isUrl()
.len(0, 2000);
if (!validator.isLength(userName, 0, 150)) {
validationErrors.push({message: "Name is too long", el: $('#user-name')});
}
if (Ghost.Validate._errors.length > 0) {
Ghost.Validate.handleErrors();
if (!validator.isLength(userBio, 0, 200)) {
validationErrors.push({message: "Bio is too long", el: $('#user-bio')});
}
if (!validator.isEmail(userEmail)) {
validationErrors.push({message: "Please supply a valid email address", el: $('#user-email')});
}
if (!validator.isLength(userLocation, 0, 150)) {
validationErrors.push({message: "Location is too long", el: $('#user-location')});
}
if (userWebsite.length) {
if (!validator.isURL(userWebsite) || !validator.isLength(userWebsite, 0, 2000)) {
validationErrors.push({message: "Please use a valid url", el: $('#user-website')});
}
}
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
this.model.save({
@ -389,16 +396,20 @@
var self = this,
oldPassword = this.$('#user-password-old').val(),
newPassword = this.$('#user-password-new').val(),
ne2Password = this.$('#user-new-password-verification').val();
ne2Password = this.$('#user-new-password-verification').val(),
validationErrors = [];
Ghost.Validate._errors = [];
Ghost.Validate.check(newPassword, {message: 'Your new passwords do not match'}).equals(ne2Password);
Ghost.Validate.check(newPassword, {message: 'Your password is not long enough. It must be at least 8 characters long.'}).len(8);
if (!validator.equals(newPassword, ne2Password)) {
validationErrors.push("Your new passwords do not match");
}
if (Ghost.Validate._errors.length > 0) {
Ghost.Validate.handleErrors();
if (!validator.isLength(newPassword, 8)) {
validationErrors.push("Your password is not long enough. It must be at least 8 characters long.");
}
if (validationErrors.length) {
validator.handleErrors(validationErrors);
} else {
$.ajax({
url: Ghost.paths.subdir + '/ghost/changepw/',
type: 'POST',

View File

@ -23,7 +23,7 @@
"email": {
"defaultValue": "ghost@example.com",
"validations": {
"notNull": true,
"isNull": false,
"isEmail": true
}
},
@ -36,29 +36,29 @@
"defaultLang": {
"defaultValue": "en_US",
"validations": {
"notNull": true
"isNull": false
}
},
"postsPerPage": {
"defaultValue": "6",
"validations": {
"notNull": true,
"isNull": false,
"isInt": true,
"max": 1000
"isLength": [0, 1000]
}
},
"forceI18n": {
"defaultValue": "true",
"validations": {
"notNull": true,
"isIn": ["true", "false"]
"isNull": false,
"isIn": [["true", "false"]]
}
},
"permalinks": {
"defaultValue": "/:slug/",
"validations": {
"is": "^(\/:?[a-z0-9_-]+){1,5}\/$",
"regex": "(:id|:slug|:year|:month|:day)",
"matches": "^(\/:?[a-z0-9_-]+){1,5}\/$",
"matches": "(:id|:slug|:year|:month|:day)",
"notContains": "/ghost/"
}
}

View File

@ -8,7 +8,7 @@ var db = {
html: {type: 'text', maxlength: 16777215, fieldtype: 'medium', nullable: true},
image: {type: 'text', maxlength: 2000, nullable: true},
featured: {type: 'bool', nullable: false, defaultTo: false},
page: {type: 'bool', nullable: false, defaultTo: false, validations: {'isIn': ['true', 'false']}},
page: {type: 'bool', nullable: false, defaultTo: false, validations: {'isIn': [['true', 'false']]}},
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'draft'},
language: {type: 'string', maxlength: 6, nullable: false, defaultTo: 'en_US'},
meta_title: {type: 'string', maxlength: 150, nullable: true},
@ -31,7 +31,7 @@ var db = {
image: {type: 'text', maxlength: 2000, nullable: true},
cover: {type: 'text', maxlength: 2000, nullable: true},
bio: {type: 'string', maxlength: 200, nullable: true},
website: {type: 'text', maxlength: 2000, nullable: true, validations: {'isUrl': true}},
website: {type: 'text', maxlength: 2000, nullable: true, validations: {'isURL': true}},
location: {type: 'text', maxlength: 65535, nullable: true},
accessibility: {type: 'text', maxlength: 65535, nullable: true},
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'active'},
@ -91,7 +91,7 @@ var db = {
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {'isUUID': true}},
key: {type: 'string', maxlength: 150, nullable: false, unique: true},
value: {type: 'text', maxlength: 65535, nullable: true},
type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'core', validations: {'isIn': ['core', 'blog', 'theme', 'app', 'plugin']}},
type: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'core', validations: {'isIn': [['core', 'blog', 'theme', 'app', 'plugin']]}},
created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'integer', nullable: false},
updated_at: {type: 'dateTime', nullable: true},

View File

@ -6,6 +6,16 @@ var schema = require('../schema').tables,
validateSettings,
validate;
// Provide a few custom validators
//
validator.extend('empty', function (str) {
return _.isEmpty(str);
});
validator.extend('notContains', function (str, badString) {
return !_.contains(str, badString);
});
// Validation validation against schema attributes
// values are checked against the validation objects
// form schema.js
@ -16,17 +26,18 @@ validateSchema = function (tableName, model) {
// check nullable
if (model.hasOwnProperty(columnKey) && schema[tableName][columnKey].hasOwnProperty('nullable')
&& schema[tableName][columnKey].nullable !== true) {
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
'] cannot be blank.').notNull();
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
'] cannot be blank.').notEmpty();
if (validator.isNull(model[columnKey]) || validator.empty(model[columnKey])) {
throw new Error('Value in [' + tableName + '.' + columnKey + '] cannot be blank.');
}
}
// TODO: check if mandatory values should be enforced
if (model[columnKey]) {
// check length
if (schema[tableName][columnKey].hasOwnProperty('maxlength')) {
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
'] exceeds maximum length of %2 characters.').len(0, schema[tableName][columnKey].maxlength);
if (!validator.isLength(model[columnKey], 0, schema[tableName][columnKey].maxlength)) {
throw new Error('Value in [' + tableName + '.' + columnKey +
'] exceeds maximum length of ' + schema[tableName][columnKey].maxlength + ' characters.');
}
}
//check validations objects
@ -36,9 +47,8 @@ validateSchema = function (tableName, model) {
//check type
if (schema[tableName][columnKey].hasOwnProperty('type')) {
if (schema[tableName][columnKey].type === 'integer') {
validator.check(model[columnKey], 'Value in [' + tableName + '.' + columnKey +
'] is no valid integer.' + model[columnKey]).isInt();
if (schema[tableName][columnKey].type === 'integer' && !validator.isInt(model[columnKey])) {
throw new Error('Value in [' + tableName + '.' + columnKey + '] is no valid integer.');
}
}
}
@ -57,29 +67,42 @@ validateSettings = function (defaultSettings, model) {
}
};
// Validate using the validation module.
// Each validation's key is a name and its value is an array of options
// Use true (boolean) if options aren't applicable
// Validate default settings using the validator module.
// Each validation's key is a method name and its value is an array of options
//
// eg:
// validations: { isUrl: true, len: [20, 40] }
// validations: { isUrl: true, isLength: [20, 40] }
//
// will validate that a values's length is a URL between 20 and 40 chars,
// available validators: https://github.com/chriso/node-validator#list-of-validation-methods
// will validate that a setting's length is a URL between 20 and 40 chars.
//
// If you pass a boolean as the value, it will specify the "good" result. By default
// the "good" result is assumed to be true.
//
// eg:
// validations: { isNull: false } // means the "good" result would
// // fail the `isNull` check, so
// // not null.
//
// available validators: https://github.com/chriso/validator.js#validators
validate = function (value, key, validations) {
_.each(validations, function (validationOptions, validationName) {
var validation = validator.check(value, 'Validation [' + validationName + '] of field [' + key + '] failed.');
var goodResult = true;
if (validationOptions === true) {
validationOptions = null;
}
/* jshint ignore:start */
if (typeof validationOptions !== 'array') {
if (_.isBoolean(validationOptions)) {
goodResult = validationOptions;
validationOptions = [];
} else if (!_.isArray(validationOptions)) {
validationOptions = [validationOptions];
}
/* jshint ignore:end */
// equivalent of validation.isSomething(option1, option2)
validation[validationName].apply(validation, validationOptions);
validationOptions.unshift(value);
// equivalent of validator.isSomething(option1, option2)
if (validator[validationName].apply(validator, validationOptions) !== goodResult) {
throw new Error('Settings validation (' + validationName + ') failed for ' + key);
}
validationOptions.shift();
}, this);
};

View File

@ -17,7 +17,9 @@ var _ = require('lodash'),
function validatePasswordLength(password) {
try {
validator.check(password, "Your password must be at least 8 characters long.").len(8);
if (!validator.isLength(password, 8)) {
throw new Error('Your password must be at least 8 characters long.');
}
} catch (error) {
return when.reject(error);
}

View File

@ -91,6 +91,27 @@ CasperTest.begin("Word count and plurality", 4, function suite(test) {
});
});
CasperTest.begin('Required Title', 4, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time");
});
casper.waitForSelector('#entry-title', function then() {
test.assertEvalEquals(function() {
return document.getElementById('entry-title').value;
}, '', 'Title is empty');
});
casper.thenClick('.js-publish-button'); // Safe to assume draft mode?
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'must specify a title');
}, function onTimeout() {
test.fail('Title required error did not appear');
}, 2000);
});
CasperTest.begin('Title Trimming', 2, function suite(test) {
var untrimmedTitle = ' test title ',
trimmedTitle = 'test title';

View File

@ -130,3 +130,24 @@ CasperTest.begin("Can login to Ghost", 4, function suite(test) {
test.fail('Failed to load ghost/ resource');
});
}, true);
CasperTest.begin('Ensure email field form validation', 1, function suite(test) {
casper.thenOpen(url + 'ghost/signin/');
casper.waitForOpaque(".js-login-box",
function then() {
this.fill("form.login-form", {
'email': 'notanemail'
}, true);
},
function onTimeout() {
test.fail('Login form didn\'t fade in.');
});
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'Invalid Email');
}, function onTimeout() {
test.fail('Email validation error did not appear');
}, 2000);
}, true);

View File

@ -112,6 +112,111 @@ CasperTest.begin("Settings screen is correct", 18, function suite(test) {
});
});
CasperTest.begin('Ensure general blog title field length validation', 3, function suite(test) {
casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time");
});
casper.waitForSelector('#general', function then() {
this.fill("form#settings-general", {
'general[title]': new Array(152).join('a')
});
});
casper.thenClick('#general .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'too long');
}, function onTimeout() {
test.fail('Blog title length error did not appear');
}, 2000);
});
CasperTest.begin('Ensure general blog description field length validation', 3, function suite(test) {
casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time");
});
casper.waitForSelector('#general', function then() {
this.fillSelectors("form#settings-general", {
'#blog-description': new Array(202).join('a')
});
});
casper.thenClick('#general .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'too long');
}, function onTimeout() {
test.fail('Blog description length error did not appear');
}, 2000);
});
CasperTest.begin('Ensure postsPerPage number field form validation', 3, function suite(test) {
casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time");
});
casper.waitForSelector('#general', function then() {
this.fill("form#settings-general", {
'general[postsPerPage]': 'notaninteger'
});
});
casper.thenClick('#general .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'use a number');
}, function onTimeout() {
test.fail('postsPerPage error did not appear');
}, 2000);
});
CasperTest.begin('Ensure postsPerPage max of 1000', 3, function suite(test) {
casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time");
});
casper.waitForSelector('#general', function then() {
this.fill("form#settings-general", {
'general[postsPerPage]': '1001'
});
});
casper.thenClick('#general .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'use a number less than 1000');
}, function onTimeout() {
test.fail('postsPerPage max error did not appear');
}, 2000);
});
CasperTest.begin('Ensure postsPerPage min of 0', 3, function suite(test) {
casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time");
});
casper.waitForSelector('#general', function then() {
this.fill("form#settings-general", {
'general[postsPerPage]': '-1'
});
});
casper.thenClick('#general .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'use a number greater than 0');
}, function onTimeout() {
test.fail('postsPerPage min error did not appear');
}, 2000);
});
CasperTest.begin("User settings screen validates email", 6, function suite(test) {
var email, brokenEmail;
@ -183,3 +288,66 @@ CasperTest.begin("User settings screen shows remaining characters for Bio proper
test.assert(getRemainingBioCharacterCount() === '195', 'Bio remaining characters is 195');
});
});
CasperTest.begin('Ensure user bio field length validation', 3, function suite(test) {
casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time");
});
casper.waitForSelector('#user', function then() {
this.fillSelectors("form.user-profile", {
'#user-bio': new Array(202).join('a')
});
});
casper.thenClick('#user .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'is too long');
}, function onTimeout() {
test.fail('Bio field length error did not appear');
}, 2000);
});
CasperTest.begin('Ensure user url field validation', 3, function suite(test) {
casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time");
});
casper.waitForSelector('#user', function then() {
this.fillSelectors("form.user-profile", {
'#user-website': 'notaurl'
});
});
casper.thenClick('#user .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'use a valid url');
}, function onTimeout() {
test.fail('Url validation error did not appear');
}, 2000);
});
CasperTest.begin('Ensure user location field length validation', 3, function suite(test) {
casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time");
});
casper.waitForSelector('#user', function then() {
this.fillSelectors("form.user-profile", {
'#user-location': new Array(1002).join('a')
});
});
casper.thenClick('#user .button-save');
casper.waitForSelectorTextChange('.notification-error', function onSuccess() {
test.assertSelectorHasText('.notification-error', 'is too long');
}, function onTimeout() {
test.fail('Location field length error did not appear');
}, 2000);
});

View File

@ -52,7 +52,7 @@
"showdown": "0.3.1",
"sqlite3": "2.2.0",
"unidecode": "0.1.3",
"validator": "1.4.0",
"validator": "3.4.0",
"when": "2.7.0"
},
"optionalDependencies": {