mirror of
https://github.com/TryGhost/Ghost-Admin.git
synced 2023-12-14 02:33:04 +01:00
Fix nav regressions in admin client
issue #5841 - fix relative link checks in navlink url input component - fix navlink url input component sending absolute URLs instead of relative URLs to action handler - remove URL manipulation in navigation settings controller (url input handles URL manipulation, validator flags anything that's still incorrect) - capture cmd-s in url input to ensure changes are actioned before save - automatically add mailto: to e-mail addresses - add gh-validation-state-container component so .error/.success validation classes can be applied to any container element - add validation-state mixin that can be mixed in to any other component to give it access to validation status (used in gh-navitem component to keep alignment when inline error message elements are added) - validate and display inline errors on save - improve ember test coverage for navigation settings related controller and components
This commit is contained in:
parent
d1bf239c9d
commit
4ebacc7d9c
21 changed files with 1070 additions and 199 deletions
|
@ -1,32 +1,5 @@
|
|||
import Ember from 'ember';
|
||||
import ValidationStatusContainer from 'ghost/components/gh-validation-status-container';
|
||||
|
||||
/**
|
||||
* Handles the CSS necessary to show a specific property state. When passed a
|
||||
* DS.Errors object and a property name, if the DS.Errors object has errors for
|
||||
* the specified property, it will change the CSS to reflect the error state
|
||||
* @param {DS.Errors} errors The DS.Errors object
|
||||
* @param {string} property Name of the property
|
||||
*/
|
||||
export default Ember.Component.extend({
|
||||
classNames: 'form-group',
|
||||
classNameBindings: ['errorClass'],
|
||||
|
||||
errors: null,
|
||||
property: '',
|
||||
hasValidated: Ember.A(),
|
||||
|
||||
errorClass: Ember.computed('errors.[]', 'property', 'hasValidated.[]', function () {
|
||||
var property = this.get('property'),
|
||||
errors = this.get('errors'),
|
||||
hasValidated = this.get('hasValidated');
|
||||
|
||||
// If we haven't yet validated this field, there is no validation class needed
|
||||
if (!hasValidated || !hasValidated.contains(property)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (errors) {
|
||||
return errors.get(property) ? 'error' : 'success';
|
||||
}
|
||||
})
|
||||
export default ValidationStatusContainer.extend({
|
||||
classNames: 'form-group'
|
||||
});
|
||||
|
|
|
@ -9,6 +9,8 @@ export default Ember.Component.extend({
|
|||
navElements = '.gh-blognav-item:not(.gh-blognav-item:last-child)',
|
||||
self = this;
|
||||
|
||||
this._super(...arguments);
|
||||
|
||||
navContainer.sortable({
|
||||
handle: '.gh-blognav-grab',
|
||||
items: navElements,
|
||||
|
@ -28,6 +30,6 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
willDestroyElement: function () {
|
||||
this.$('.js-gh-blognav').sortable('destroy');
|
||||
this.$('.ui-sortable').sortable('destroy');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
function joinUrlParts(url, path) {
|
||||
var joinUrlParts,
|
||||
isRelative;
|
||||
|
||||
joinUrlParts = function (url, path) {
|
||||
if (path[0] !== '/' && url.slice(-1) !== '/') {
|
||||
path = '/' + path;
|
||||
} else if (path[0] === '/' && url.slice(-1) === '/') {
|
||||
|
@ -8,9 +11,16 @@ function joinUrlParts(url, path) {
|
|||
}
|
||||
|
||||
return url + path;
|
||||
}
|
||||
};
|
||||
|
||||
isRelative = function (url) {
|
||||
// "protocol://", "//example.com", "scheme:", "#anchor", & invalid paths
|
||||
// should all be treated as absolute
|
||||
return !url.match(/\s/) && !validator.isURL(url) && !url.match(/^(\/\/|#|[a-zA-Z0-9\-]+:)/);
|
||||
};
|
||||
|
||||
export default Ember.TextField.extend({
|
||||
classNames: 'gh-input',
|
||||
classNameBindings: ['fakePlaceholder'],
|
||||
|
||||
didReceiveAttrs: function () {
|
||||
|
@ -18,8 +28,7 @@ export default Ember.TextField.extend({
|
|||
baseUrl = this.get('baseUrl');
|
||||
|
||||
// if we have a relative url, create the absolute url to be displayed in the input
|
||||
// if (this.get('isRelative')) {
|
||||
if (!validator.isURL(url) && url.indexOf('mailto:') !== 0) {
|
||||
if (isRelative(url)) {
|
||||
url = joinUrlParts(baseUrl, url);
|
||||
}
|
||||
|
||||
|
@ -34,10 +43,6 @@ export default Ember.TextField.extend({
|
|||
return this.get('isBaseUrl') && this.get('last') && !this.get('hasFocus');
|
||||
}),
|
||||
|
||||
isRelative: Ember.computed('value', function () {
|
||||
return !validator.isURL(this.get('value')) && this.get('value').indexOf('mailto:') !== 0;
|
||||
}),
|
||||
|
||||
focusIn: function (event) {
|
||||
this.set('hasFocus', true);
|
||||
|
||||
|
@ -58,6 +63,11 @@ export default Ember.TextField.extend({
|
|||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// CMD-S
|
||||
if (event.keyCode === 83 && event.metaKey) {
|
||||
this.notifyUrlChanged();
|
||||
}
|
||||
},
|
||||
|
||||
keyPress: function (event) {
|
||||
|
@ -80,11 +90,35 @@ export default Ember.TextField.extend({
|
|||
this.set('value', this.get('value').trim());
|
||||
|
||||
var url = this.get('value'),
|
||||
baseUrl = this.get('baseUrl');
|
||||
urlParts = document.createElement('a'),
|
||||
baseUrl = this.get('baseUrl'),
|
||||
baseUrlParts = document.createElement('a');
|
||||
|
||||
// leverage the browser's native URI parsing
|
||||
urlParts.href = url;
|
||||
baseUrlParts.href = baseUrl;
|
||||
|
||||
// if we have an email address, add the mailto:
|
||||
if (validator.isEmail(url)) {
|
||||
url = `mailto:${url}`;
|
||||
this.set('value', url);
|
||||
}
|
||||
|
||||
// if we have a relative url, create the absolute url to be displayed in the input
|
||||
if (this.get('isRelative')) {
|
||||
this.set('value', joinUrlParts(baseUrl, url));
|
||||
if (isRelative(url)) {
|
||||
url = joinUrlParts(baseUrl, url);
|
||||
this.set('value', url);
|
||||
}
|
||||
|
||||
// remove the base url before sending to action
|
||||
if (urlParts.host === baseUrlParts.host && !url.match(/^#/)) {
|
||||
url = url.replace(/^[a-zA-Z0-9\-]+:/, '');
|
||||
url = url.replace(/^\/\//, '');
|
||||
url = url.replace(baseUrlParts.host, '');
|
||||
url = url.replace(baseUrlParts.pathname, '');
|
||||
if (!url.match(/^\//)) {
|
||||
url = '/' + url;
|
||||
}
|
||||
}
|
||||
|
||||
this.sendAction('change', url);
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
import Ember from 'ember';
|
||||
import ValidationStateMixin from 'ghost/mixins/validation-state';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
export default Ember.Component.extend(ValidationStateMixin, {
|
||||
classNames: 'gh-blognav-item',
|
||||
classNameBindings: ['errorClass'],
|
||||
|
||||
attributeBindings: ['order:data-order'],
|
||||
order: Ember.computed.readOnly('navItem.order'),
|
||||
errors: Ember.computed.readOnly('navItem.errors'),
|
||||
|
||||
errorClass: Ember.computed('hasError', function () {
|
||||
if (this.get('hasError')) {
|
||||
return 'gh-blognav-item--error';
|
||||
}
|
||||
}),
|
||||
|
||||
keyPress: function (event) {
|
||||
// enter key
|
||||
|
@ -12,6 +21,8 @@ export default Ember.Component.extend({
|
|||
event.preventDefault();
|
||||
this.send('addItem');
|
||||
}
|
||||
|
||||
this.get('navItem.errors').clear();
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
17
app/components/gh-validation-status-container.js
Normal file
17
app/components/gh-validation-status-container.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import Ember from 'ember';
|
||||
import ValidationStateMixin from 'ghost/mixins/validation-state';
|
||||
|
||||
/**
|
||||
* Handles the CSS necessary to show a specific property state. When passed a
|
||||
* DS.Errors object and a property name, if the DS.Errors object has errors for
|
||||
* the specified property, it will change the CSS to reflect the error state
|
||||
* @param {DS.Errors} errors The DS.Errors object
|
||||
* @param {string} property Name of the property
|
||||
*/
|
||||
export default Ember.Component.extend(ValidationStateMixin, {
|
||||
classNameBindings: ['errorClass'],
|
||||
|
||||
errorClass: Ember.computed('hasError', function () {
|
||||
return this.get('hasError') ? 'error' : 'success';
|
||||
})
|
||||
});
|
|
@ -1,14 +1,24 @@
|
|||
import Ember from 'ember';
|
||||
import DS from 'ember-data';
|
||||
import SettingsSaveMixin from 'ghost/mixins/settings-save';
|
||||
import ValidationEngine from 'ghost/mixins/validation-engine';
|
||||
|
||||
var NavItem = Ember.Object.extend({
|
||||
export const NavItem = Ember.Object.extend(ValidationEngine, {
|
||||
label: '',
|
||||
url: '',
|
||||
last: false,
|
||||
|
||||
validationType: 'navItem',
|
||||
|
||||
isComplete: Ember.computed('label', 'url', function () {
|
||||
return !(Ember.isBlank(this.get('label').trim()) || Ember.isBlank(this.get('url')));
|
||||
})
|
||||
}),
|
||||
|
||||
init: function () {
|
||||
this._super(...arguments);
|
||||
this.set('errors', DS.Errors.create());
|
||||
this.set('hasValidated', Ember.A());
|
||||
}
|
||||
});
|
||||
|
||||
export default Ember.Controller.extend(SettingsSaveMixin, {
|
||||
|
@ -57,58 +67,38 @@ export default Ember.Controller.extend(SettingsSaveMixin, {
|
|||
|
||||
save: function () {
|
||||
var navSetting,
|
||||
blogUrl = this.get('config').blogUrl,
|
||||
blogUrlRegex = new RegExp('^' + blogUrl + '(.*)', 'i'),
|
||||
navItems = this.get('navigationItems'),
|
||||
message = 'One of your navigation items has an empty label. ' +
|
||||
'<br /> Please enter a new label or delete the item before saving.',
|
||||
match,
|
||||
notifications = this.get('notifications');
|
||||
notifications = this.get('notifications'),
|
||||
validationPromises,
|
||||
self = this;
|
||||
|
||||
// Don't save if there's a blank label.
|
||||
if (navItems.find(function (item) {return !item.get('isComplete') && !item.get('last');})) {
|
||||
notifications.showAlert(message.htmlSafe(), {type: 'error'});
|
||||
return;
|
||||
}
|
||||
validationPromises = navItems.map(function (item) {
|
||||
return item.validate();
|
||||
});
|
||||
|
||||
navSetting = navItems.map(function (item) {
|
||||
var label,
|
||||
url;
|
||||
return Ember.RSVP.all(validationPromises).then(function () {
|
||||
navSetting = navItems.map(function (item) {
|
||||
var label = item.get('label').trim(),
|
||||
url = item.get('url').trim();
|
||||
|
||||
if (!item || !item.get('isComplete')) {
|
||||
return;
|
||||
}
|
||||
|
||||
label = item.get('label').trim();
|
||||
url = item.get('url').trim();
|
||||
|
||||
// is this an internal URL?
|
||||
match = url.match(blogUrlRegex);
|
||||
|
||||
if (match) {
|
||||
url = match[1];
|
||||
|
||||
// if the last char is not a slash, then add one,
|
||||
// as long as there is no # or . in the URL (anchor or file extension)
|
||||
// this also handles the empty case for the homepage
|
||||
if (url[url.length - 1] !== '/' && url.indexOf('#') === -1 && url.indexOf('.') === -1) {
|
||||
url += '/';
|
||||
if (item.get('last') && !item.get('isComplete')) {
|
||||
return null;
|
||||
}
|
||||
} else if (!validator.isURL(url) && url !== '' && url[0] !== '/' && url.indexOf('mailto:') !== 0) {
|
||||
url = '/' + url;
|
||||
}
|
||||
|
||||
return {label: label, url: url};
|
||||
}).compact();
|
||||
return {label: label, url: url};
|
||||
}).compact();
|
||||
|
||||
this.set('model.navigation', JSON.stringify(navSetting));
|
||||
self.set('model.navigation', JSON.stringify(navSetting));
|
||||
|
||||
// trigger change event because even if the final JSON is unchanged
|
||||
// we need to have navigationItems recomputed.
|
||||
this.get('model').notifyPropertyChange('navigation');
|
||||
// trigger change event because even if the final JSON is unchanged
|
||||
// we need to have navigationItems recomputed.
|
||||
self.get('model').notifyPropertyChange('navigation');
|
||||
|
||||
return this.get('model').save().catch(function (err) {
|
||||
notifications.showErrors(err);
|
||||
return self.get('model').save().catch(function (err) {
|
||||
notifications.showErrors(err);
|
||||
});
|
||||
}).catch(function () {
|
||||
// TODO: noop - needed to satisfy spinner button
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -145,12 +135,6 @@ export default Ember.Controller.extend(SettingsSaveMixin, {
|
|||
return;
|
||||
}
|
||||
|
||||
if (Ember.isBlank(url)) {
|
||||
navItem.set('url', this.get('blogUrl'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
navItem.set('url', url);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import SettingValidator from 'ghost/validators/setting';
|
|||
import ResetValidator from 'ghost/validators/reset';
|
||||
import UserValidator from 'ghost/validators/user';
|
||||
import TagSettingsValidator from 'ghost/validators/tag-settings';
|
||||
import NavItemValidator from 'ghost/validators/nav-item';
|
||||
|
||||
// our extensions to the validator library
|
||||
ValidatorExtensions.init();
|
||||
|
@ -35,7 +36,8 @@ export default Ember.Mixin.create({
|
|||
setting: SettingValidator,
|
||||
reset: ResetValidator,
|
||||
user: UserValidator,
|
||||
tag: TagSettingsValidator
|
||||
tag: TagSettingsValidator,
|
||||
navItem: NavItemValidator
|
||||
},
|
||||
|
||||
// This adds the Errors object to the validation engine, and shouldn't affect
|
||||
|
|
31
app/mixins/validation-state.js
Normal file
31
app/mixins/validation-state.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
|
||||
errors: null,
|
||||
property: '',
|
||||
hasValidated: Ember.A(),
|
||||
|
||||
hasError: Ember.computed('errors.[]', 'property', 'hasValidated.[]', function () {
|
||||
var property = this.get('property'),
|
||||
errors = this.get('errors'),
|
||||
hasValidated = this.get('hasValidated');
|
||||
|
||||
// if we aren't looking at a specific property we always want an error class
|
||||
if (!property && !Ember.isEmpty(errors)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we haven't yet validated this field, there is no validation class needed
|
||||
if (!hasValidated || !hasValidated.contains(property)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (errors) {
|
||||
return errors.get(property);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
|
||||
});
|
|
@ -16,6 +16,15 @@
|
|||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.gh-blognav-item--error {
|
||||
margin-bottom: calc(1em + 10px);
|
||||
}
|
||||
|
||||
.gh-blognav-item .response {
|
||||
position: absolute;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.gh-blognav-grab {
|
||||
padding: 0 16px 0 0;
|
||||
width: 16px;
|
||||
|
|
|
@ -37,7 +37,7 @@ input {
|
|||
user-select: text;
|
||||
}
|
||||
|
||||
.form-group.error .response {
|
||||
.error .response {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
|
@ -123,7 +123,7 @@ select {
|
|||
}
|
||||
|
||||
.gh-input.error,
|
||||
.form-group.error .gh-input,
|
||||
.error .gh-input,
|
||||
.gh-select.error,
|
||||
select.error {
|
||||
border-color: var(--red);
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
{{/unless}}
|
||||
|
||||
<div class="gh-blognav-line">
|
||||
<span class="gh-blognav-label">
|
||||
{{gh-trim-focus-input class="gh-input" focus=navItem.last placeholder="Label" value=navItem.label}}
|
||||
</span>
|
||||
<span class="gh-blognav-url">
|
||||
{{gh-navitem-url-input class="gh-input" baseUrl=baseUrl url=navItem.url last=navItem.last change="updateUrl"}}
|
||||
</span>
|
||||
{{#gh-validation-status-container tagName="span" class="gh-blognav-label" errors=navItem.errors property="label" hasValidated=navItem.hasValidated}}
|
||||
{{gh-trim-focus-input focus=navItem.last placeholder="Label" value=navItem.label}}
|
||||
{{gh-error-message errors=navItem.errors property="label"}}
|
||||
{{/gh-validation-status-container}}
|
||||
{{#gh-validation-status-container tagName="span" class="gh-blognav-url" errors=navItem.errors property="url" hasValidated=navItem.hasValidated}}
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=navItem.url last=navItem.last change="updateUrl"}}
|
||||
{{gh-error-message errors=navItem.errors property="url"}}
|
||||
{{/gh-validation-status-container}}
|
||||
</div>
|
||||
|
||||
{{#if navItem.last}}
|
||||
|
|
52
app/validators/nav-item.js
Normal file
52
app/validators/nav-item.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import BaseValidator from './base';
|
||||
|
||||
export default BaseValidator.create({
|
||||
properties: ['label', 'url'],
|
||||
|
||||
label: function (model) {
|
||||
var label = model.get('label'),
|
||||
hasValidated = model.get('hasValidated');
|
||||
|
||||
if (this.canBeIgnored(model)) { return; }
|
||||
|
||||
if (validator.empty(label)) {
|
||||
model.get('errors').add('label', 'You must specify a label');
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
hasValidated.addObject('label');
|
||||
},
|
||||
|
||||
url: function (model) {
|
||||
var url = model.get('url'),
|
||||
hasValidated = model.get('hasValidated'),
|
||||
validatorOptions = {require_protocol: true},
|
||||
urlRegex = new RegExp(/^(\/|#|[a-zA-Z0-9\-]+:)/);
|
||||
|
||||
if (this.canBeIgnored(model)) { return; }
|
||||
|
||||
if (validator.empty(url)) {
|
||||
model.get('errors').add('url', 'You must specify a URL or relative path');
|
||||
this.invalidate();
|
||||
} else if (url.match(/\s/) || (!validator.isURL(url, validatorOptions) && !url.match(urlRegex))) {
|
||||
model.get('errors').add('url', 'You must specify a valid URL or relative path');
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
hasValidated.addObject('url');
|
||||
},
|
||||
|
||||
canBeIgnored: function (model) {
|
||||
var label = model.get('label'),
|
||||
url = model.get('url'),
|
||||
isLast = model.get('last');
|
||||
|
||||
// if nav item is last and completely blank, mark it valid and skip
|
||||
if (isLast && (validator.empty(url) || url === '/') && validator.empty(label)) {
|
||||
model.get('errors').clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
|
@ -19,6 +19,7 @@
|
|||
"jquery-hammerjs": "1.0.1",
|
||||
"jquery-ui": "1.11.4",
|
||||
"jqueryui-touch-punch": "furf/jquery-ui-touch-punch",
|
||||
"jquery.simulate.drag-sortable": "0.1.0",
|
||||
"keymaster": "1.6.3",
|
||||
"loader.js": "3.2.1",
|
||||
"moment": "2.10.3",
|
||||
|
|
|
@ -68,6 +68,10 @@ module.exports = function (defaults) {
|
|||
app.import('bower_components/password-generator/lib/password-generator.js');
|
||||
app.import('bower_components/blueimp-md5/js/md5.js');
|
||||
|
||||
if (app.env === 'test') {
|
||||
app.import('bower_components/jquery.simulate.drag-sortable/jquery.simulate.drag-sortable.js');
|
||||
}
|
||||
|
||||
// 'dem Styles
|
||||
app.import('bower_components/codemirror/lib/codemirror.css');
|
||||
app.import('bower_components/codemirror/theme/xq-light.css');
|
||||
|
|
71
tests/integration/components/gh-navigation-test.js
Normal file
71
tests/integration/components/gh-navigation-test.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import { describeComponent, it } from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import Ember from 'ember';
|
||||
import { NavItem } from 'ghost/controllers/settings/navigation';
|
||||
|
||||
const { run } = Ember;
|
||||
|
||||
describeComponent(
|
||||
'gh-navigation',
|
||||
'Integration : Component : gh-navigation',
|
||||
{
|
||||
integration: true
|
||||
},
|
||||
function () {
|
||||
it('renders', function () {
|
||||
this.render(hbs`{{#gh-navigation}}<div class="js-gh-blognav"><div class="gh-blognav-item"></div></div>{{/gh-navigation}}`);
|
||||
expect(this.$('section.gh-view')).to.have.length(1);
|
||||
expect(this.$('.ui-sortable')).to.have.length(1);
|
||||
});
|
||||
|
||||
it('triggers reorder action', function () {
|
||||
let navItems = [],
|
||||
expectedOldIndex = -1,
|
||||
expectedNewIndex = -1;
|
||||
|
||||
navItems.pushObject(NavItem.create({label: 'First', url: '/first'}));
|
||||
navItems.pushObject(NavItem.create({label: 'Second', url: '/second'}));
|
||||
navItems.pushObject(NavItem.create({label: 'Third', url: '/third'}));
|
||||
navItems.pushObject(NavItem.create({label: '', url: '', last: true}));
|
||||
this.set('navigationItems', navItems);
|
||||
this.set('blogUrl', 'http://localhost:2368');
|
||||
|
||||
this.on('moveItem', (oldIndex, newIndex) => {
|
||||
expect(oldIndex).to.equal(expectedOldIndex);
|
||||
expect(newIndex).to.equal(expectedNewIndex);
|
||||
});
|
||||
|
||||
run(() => {
|
||||
this.render(hbs `
|
||||
{{#gh-navigation moveItem="moveItem"}}
|
||||
<form id="settings-navigation" class="gh-blognav js-gh-blognav" novalidate="novalidate">
|
||||
{{#each navigationItems as |navItem|}}
|
||||
{{gh-navitem navItem=navItem baseUrl=blogUrl addItem="addItem" deleteItem="deleteItem" updateUrl="updateUrl"}}
|
||||
{{/each}}
|
||||
</form>
|
||||
{{/gh-navigation}}`);
|
||||
});
|
||||
|
||||
// check it renders the nav item rows
|
||||
expect(this.$('.gh-blognav-item')).to.have.length(4);
|
||||
|
||||
// move second item up one
|
||||
expectedOldIndex = 1;
|
||||
expectedNewIndex = 0;
|
||||
Ember.$(this.$('.gh-blognav-item')[1]).simulateDragSortable({
|
||||
move: -1,
|
||||
handle: '.gh-blognav-grab'
|
||||
});
|
||||
|
||||
// move second item down one
|
||||
expectedOldIndex = 1;
|
||||
expectedNewIndex = 2;
|
||||
Ember.$(this.$('.gh-blognav-item')[1]).simulateDragSortable({
|
||||
move: 1,
|
||||
handle: '.gh-blognav-grab'
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
110
tests/integration/components/gh-navitem-test.js
Normal file
110
tests/integration/components/gh-navitem-test.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import { describeComponent, it } from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import Ember from 'ember';
|
||||
import { NavItem } from 'ghost/controllers/settings/navigation';
|
||||
|
||||
const { run } = Ember;
|
||||
|
||||
describeComponent(
|
||||
'gh-navitem',
|
||||
'Integration : Component : gh-navitem',
|
||||
{
|
||||
integration: true
|
||||
},
|
||||
function () {
|
||||
beforeEach(function () {
|
||||
this.set('baseUrl', 'http://localhost:2368');
|
||||
});
|
||||
|
||||
it('renders', function () {
|
||||
this.set('navItem', NavItem.create({label: 'Test', url: '/url'}));
|
||||
|
||||
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl}}`);
|
||||
let $item = this.$('.gh-blognav-item');
|
||||
|
||||
expect($item.find('.gh-blognav-grab').length).to.equal(1);
|
||||
expect($item.find('.gh-blognav-label').length).to.equal(1);
|
||||
expect($item.find('.gh-blognav-url').length).to.equal(1);
|
||||
expect($item.find('.gh-blognav-delete').length).to.equal(1);
|
||||
|
||||
// doesn't show any errors
|
||||
expect($item.hasClass('gh-blognav-item--error')).to.be.false;
|
||||
expect($item.find('.error').length).to.equal(0);
|
||||
expect($item.find('.response:visible').length).to.equal(0);
|
||||
});
|
||||
|
||||
it('doesn\'t show drag handle for last item', function () {
|
||||
this.set('navItem', NavItem.create({label: 'Test', url: '/url', last: true}));
|
||||
|
||||
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl}}`);
|
||||
let $item = this.$('.gh-blognav-item');
|
||||
|
||||
expect($item.find('.gh-blognav-grab').length).to.equal(0);
|
||||
});
|
||||
|
||||
it('shows add button for last item', function () {
|
||||
this.set('navItem', NavItem.create({label: 'Test', url: '/url', last: true}));
|
||||
|
||||
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl}}`);
|
||||
let $item = this.$('.gh-blognav-item');
|
||||
|
||||
expect($item.find('.gh-blognav-add').length).to.equal(1);
|
||||
expect($item.find('.gh-blognav-delete').length).to.equal(0);
|
||||
});
|
||||
|
||||
it('triggers delete action', function () {
|
||||
this.set('navItem', NavItem.create({label: 'Test', url: '/url'}));
|
||||
|
||||
let deleteActionCallCount = 0;
|
||||
this.on('deleteItem', (navItem) => {
|
||||
expect(navItem).to.equal(this.get('navItem'));
|
||||
deleteActionCallCount++;
|
||||
});
|
||||
|
||||
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl deleteItem="deleteItem"}}`);
|
||||
this.$('.gh-blognav-delete').trigger('click');
|
||||
|
||||
expect(deleteActionCallCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('triggers add action', function () {
|
||||
this.set('navItem', NavItem.create({label: 'Test', url: '/url', last: true}));
|
||||
|
||||
let addActionCallCount = 0;
|
||||
this.on('add', () => { addActionCallCount++; });
|
||||
|
||||
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl addItem="add"}}`);
|
||||
this.$('.gh-blognav-add').trigger('click');
|
||||
|
||||
expect(addActionCallCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('triggers update action', function () {
|
||||
this.set('navItem', NavItem.create({label: 'Test', url: '/url'}));
|
||||
|
||||
let updateActionCallCount = 0;
|
||||
this.on('update', () => { updateActionCallCount++; });
|
||||
|
||||
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl updateUrl="update"}}`);
|
||||
this.$('.gh-blognav-url input').trigger('blur');
|
||||
|
||||
expect(updateActionCallCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('displays inline errors', function () {
|
||||
this.set('navItem', NavItem.create({label: '', url: ''}));
|
||||
this.get('navItem').validate();
|
||||
|
||||
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl}}`);
|
||||
let $item = this.$('.gh-blognav-item');
|
||||
|
||||
expect($item.hasClass('gh-blognav-item--error')).to.be.true;
|
||||
expect($item.find('.gh-blognav-label').hasClass('error')).to.be.true;
|
||||
expect($item.find('.gh-blognav-label .response').text().trim()).to.equal('You must specify a label');
|
||||
expect($item.find('.gh-blognav-url').hasClass('error')).to.be.true;
|
||||
expect($item.find('.gh-blognav-url .response').text().trim()).to.equal('You must specify a URL or relative path');
|
||||
});
|
||||
}
|
||||
);
|
332
tests/integration/components/gh-navitem-url-input-test.js
Normal file
332
tests/integration/components/gh-navitem-url-input-test.js
Normal file
|
@ -0,0 +1,332 @@
|
|||
/* jshint scripturl:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeComponent,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import Ember from 'ember';
|
||||
|
||||
const { run } = Ember,
|
||||
// we want baseUrl to match the running domain so relative URLs are
|
||||
// handled as expected (browser auto-sets the domain when using a.href)
|
||||
currentUrl = `${window.location.protocol}//${window.location.host}/`;
|
||||
|
||||
describeComponent(
|
||||
'gh-navitem-url-input',
|
||||
'Integration : Component : gh-navitem-url-input', {
|
||||
integration: true
|
||||
},
|
||||
function () {
|
||||
beforeEach(function () {
|
||||
// set defaults
|
||||
this.set('baseUrl', currentUrl);
|
||||
this.set('url', '');
|
||||
this.set('isLast', false);
|
||||
});
|
||||
|
||||
it('renders correctly with blank url', function () {
|
||||
this.render(hbs`
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
expect($input).to.have.length(1);
|
||||
expect($input.hasClass('gh-input')).to.be.true;
|
||||
expect($input.val()).to.equal(currentUrl);
|
||||
});
|
||||
|
||||
it('renders correctly with relative urls', function () {
|
||||
this.set('url', '/about');
|
||||
this.render(hbs`
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
expect($input.val()).to.equal(`${currentUrl}about`);
|
||||
|
||||
this.set('url', '/about#contact');
|
||||
expect($input.val()).to.equal(`${currentUrl}about#contact`);
|
||||
});
|
||||
|
||||
it('renders correctly with absolute urls', function () {
|
||||
this.set('url', 'https://example.com:2368/#test');
|
||||
this.render(hbs`
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
expect($input.val()).to.equal('https://example.com:2368/#test');
|
||||
|
||||
this.set('url', 'mailto:test@example.com');
|
||||
expect($input.val()).to.equal('mailto:test@example.com');
|
||||
|
||||
this.set('url', 'tel:01234-5678-90');
|
||||
expect($input.val()).to.equal('tel:01234-5678-90');
|
||||
|
||||
this.set('url', '//protocol-less-url.com');
|
||||
expect($input.val()).to.equal('//protocol-less-url.com');
|
||||
|
||||
this.set('url', '#anchor');
|
||||
expect($input.val()).to.equal('#anchor');
|
||||
});
|
||||
|
||||
it('deletes base URL on backspace', function () {
|
||||
this.render(hbs`
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
expect($input.val()).to.equal(currentUrl);
|
||||
run(() => {
|
||||
// TODO: why is ember's keyEvent helper not available here?
|
||||
let e = Ember.$.Event('keydown');
|
||||
e.keyCode = 8;
|
||||
$input.trigger(e);
|
||||
});
|
||||
expect($input.val()).to.equal('');
|
||||
});
|
||||
|
||||
it('deletes base URL on delete', function () {
|
||||
this.render(hbs`
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
expect($input.val()).to.equal(currentUrl);
|
||||
run(() => {
|
||||
// TODO: why is ember's keyEvent helper not available here?
|
||||
let e = Ember.$.Event('keydown');
|
||||
e.keyCode = 46;
|
||||
$input.trigger(e);
|
||||
});
|
||||
expect($input.val()).to.equal('');
|
||||
});
|
||||
|
||||
it('adds base url to relative urls on blur', function () {
|
||||
this.on('updateUrl', () => { return null; });
|
||||
this.render(hbs`
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
run(() => { $input.val('/about').trigger('input'); });
|
||||
run(() => { $input.trigger('blur'); });
|
||||
|
||||
expect($input.val()).to.equal(`${currentUrl}about`);
|
||||
});
|
||||
|
||||
it('adds "mailto:" to e-mail addresses on blur', function () {
|
||||
this.on('updateUrl', () => { return null; });
|
||||
this.render(hbs`
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
run(() => { $input.val('test@example.com').trigger('input'); });
|
||||
run(() => { $input.trigger('blur'); });
|
||||
|
||||
expect($input.val()).to.equal('mailto:test@example.com');
|
||||
|
||||
// ensure we don't double-up on the mailto:
|
||||
run(() => { $input.trigger('blur'); });
|
||||
expect($input.val()).to.equal('mailto:test@example.com');
|
||||
});
|
||||
|
||||
it('doesn\'t add base url to invalid urls on blur', function () {
|
||||
this.on('updateUrl', () => { return null; });
|
||||
this.render(hbs`
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
let changeValue = function (value) {
|
||||
run(() => {
|
||||
$input.val(value).trigger('input').trigger('blur');
|
||||
});
|
||||
};
|
||||
|
||||
changeValue('with spaces');
|
||||
expect($input.val()).to.equal('with spaces');
|
||||
|
||||
changeValue('/with spaces');
|
||||
expect($input.val()).to.equal('/with spaces');
|
||||
});
|
||||
|
||||
it('doesn\'t mangle invalid urls on blur', function () {
|
||||
this.on('updateUrl', () => { return null; });
|
||||
this.render(hbs`
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
run(() => {
|
||||
$input.val(`${currentUrl} /test`).trigger('input').trigger('blur');
|
||||
});
|
||||
|
||||
expect($input.val()).to.equal(`${currentUrl} /test`);
|
||||
});
|
||||
|
||||
it('toggles .fake-placeholder on focus', function () {
|
||||
this.set('isLast', true);
|
||||
this.render(hbs `
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
expect($input.hasClass('fake-placeholder')).to.be.true;
|
||||
|
||||
run(() => { $input.trigger('focus'); });
|
||||
expect($input.hasClass('fake-placeholder')).to.be.false;
|
||||
});
|
||||
|
||||
it('triggers "change" action on blur', function () {
|
||||
let changeActionCallCount = 0;
|
||||
this.on('updateUrl', () => { changeActionCallCount++; });
|
||||
|
||||
this.render(hbs `
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
$input.trigger('blur');
|
||||
|
||||
expect(changeActionCallCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('triggers "change" action on enter', function () {
|
||||
let changeActionCallCount = 0;
|
||||
this.on('updateUrl', () => { changeActionCallCount++; });
|
||||
|
||||
this.render(hbs `
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
run(() => {
|
||||
// TODO: why is ember's keyEvent helper not available here?
|
||||
let e = Ember.$.Event('keypress');
|
||||
e.keyCode = 13;
|
||||
$input.trigger(e);
|
||||
});
|
||||
|
||||
expect(changeActionCallCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('triggers "change" action on CMD-S', function () {
|
||||
let changeActionCallCount = 0;
|
||||
this.on('updateUrl', () => { changeActionCallCount++; });
|
||||
|
||||
this.render(hbs `
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
run(() => {
|
||||
// TODO: why is ember's keyEvent helper not available here?
|
||||
let e = Ember.$.Event('keydown');
|
||||
e.keyCode = 83;
|
||||
e.metaKey = true;
|
||||
$input.trigger(e);
|
||||
});
|
||||
|
||||
expect(changeActionCallCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('sends absolute urls straight through to change action', function () {
|
||||
let expectedUrl = '';
|
||||
|
||||
this.on('updateUrl', (url) => {
|
||||
expect(url).to.equal(expectedUrl);
|
||||
});
|
||||
|
||||
this.render(hbs `
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
let testUrl = (url) => {
|
||||
expectedUrl = url;
|
||||
run(() => { $input.val(url).trigger('input'); });
|
||||
run(() => { $input.trigger('blur'); });
|
||||
};
|
||||
|
||||
testUrl('http://example.com');
|
||||
testUrl('http://example.com/');
|
||||
testUrl('https://example.com');
|
||||
testUrl('//example.com');
|
||||
testUrl('//localhost:1234');
|
||||
testUrl('#anchor');
|
||||
testUrl('mailto:test@example.com');
|
||||
testUrl('tel:12345-567890');
|
||||
testUrl('javascript:alert("testing");');
|
||||
});
|
||||
|
||||
it('strips base url from relative urls before sending to change action', function () {
|
||||
let expectedUrl = '';
|
||||
|
||||
this.on('updateUrl', (url) => {
|
||||
expect(url).to.equal(expectedUrl);
|
||||
});
|
||||
|
||||
this.render(hbs `
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
let testUrl = (url) => {
|
||||
expectedUrl = `/${url}`;
|
||||
run(() => { $input.val(`${currentUrl}${url}`).trigger('input'); });
|
||||
run(() => { $input.trigger('blur'); });
|
||||
};
|
||||
|
||||
testUrl('about');
|
||||
testUrl('about#contact');
|
||||
testUrl('test/nested');
|
||||
});
|
||||
|
||||
it('handles a baseUrl with a path component', function () {
|
||||
let expectedUrl = '';
|
||||
|
||||
this.set('baseUrl', `${currentUrl}blog/`);
|
||||
|
||||
this.on('updateUrl', (url) => {
|
||||
expect(url).to.equal(expectedUrl);
|
||||
});
|
||||
|
||||
this.render(hbs `
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
let testUrl = (url) => {
|
||||
expectedUrl = url;
|
||||
run(() => { $input.val(`${currentUrl}blog${url}`).trigger('input'); });
|
||||
run(() => { $input.trigger('blur'); });
|
||||
};
|
||||
|
||||
testUrl('/about');
|
||||
testUrl('/about#contact');
|
||||
testUrl('/test/nested');
|
||||
});
|
||||
|
||||
it('handles links to subdomains of blog domain', function () {
|
||||
let expectedUrl = '';
|
||||
|
||||
this.set('baseUrl', 'http://example.com/');
|
||||
|
||||
this.on('updateUrl', (url) => {
|
||||
expect(url).to.equal(expectedUrl);
|
||||
});
|
||||
|
||||
this.render(hbs `
|
||||
{{gh-navitem-url-input baseUrl=baseUrl url=url last=isLast change="updateUrl"}}
|
||||
`);
|
||||
let $input = this.$('input');
|
||||
|
||||
expectedUrl = 'http://test.example.com/';
|
||||
run(() => { $input.val(expectedUrl).trigger('input').trigger('blur'); });
|
||||
expect($input.val()).to.equal(expectedUrl);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -1,27 +0,0 @@
|
|||
/* jshint expr:true */
|
||||
import {expect} from 'chai';
|
||||
import {
|
||||
describeComponent,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
|
||||
describeComponent(
|
||||
'gh-navigation',
|
||||
'GhNavigationComponent',
|
||||
{
|
||||
// specify the other units that are required for this test
|
||||
// needs: ['component:foo', 'helper:bar']
|
||||
},
|
||||
function () {
|
||||
it('renders', function () {
|
||||
// creates the component instance
|
||||
var component = this.subject();
|
||||
|
||||
expect(component._state).to.equal('preRender');
|
||||
|
||||
// renders the component on the page
|
||||
this.render();
|
||||
expect(component._state).to.equal('inDOM');
|
||||
});
|
||||
}
|
||||
);
|
|
@ -11,75 +11,6 @@ describeComponent(
|
|||
'GhNavitemUrlInputComponent',
|
||||
{},
|
||||
function () {
|
||||
it('renders', function () {
|
||||
var component = this.subject();
|
||||
|
||||
expect(component._state).to.equal('preRender');
|
||||
|
||||
this.render();
|
||||
|
||||
expect(component._state).to.equal('inDOM');
|
||||
});
|
||||
|
||||
it('renders correctly with a URL that matches the base URL', function () {
|
||||
var component = this.subject({
|
||||
baseUrl: 'http://example.com/'
|
||||
});
|
||||
|
||||
Ember.run(function () {
|
||||
component.set('value', 'http://example.com/');
|
||||
});
|
||||
|
||||
this.render();
|
||||
|
||||
expect(this.$().val()).to.equal('http://example.com/');
|
||||
});
|
||||
|
||||
it('renders correctly with a relative URL', function () {
|
||||
var component = this.subject({
|
||||
baseUrl: 'http://example.com/'
|
||||
});
|
||||
|
||||
Ember.run(function () {
|
||||
component.set('value', '/go/docs');
|
||||
});
|
||||
|
||||
this.render();
|
||||
|
||||
expect(this.$().val()).to.equal('/go/docs');
|
||||
});
|
||||
|
||||
it('renders correctly with a mailto URL', function () {
|
||||
var component = this.subject({
|
||||
baseUrl: 'http://example.com/'
|
||||
});
|
||||
|
||||
Ember.run(function () {
|
||||
component.set('value', 'mailto:someone@example.com');
|
||||
});
|
||||
|
||||
this.render();
|
||||
|
||||
expect(this.$().val()).to.equal('mailto:someone@example.com');
|
||||
});
|
||||
|
||||
it('identifies a URL as relative', function () {
|
||||
var component = this.subject({
|
||||
baseUrl: 'http://example.com/',
|
||||
url: '/go/docs'
|
||||
});
|
||||
|
||||
this.render();
|
||||
|
||||
expect(component.get('isRelative')).to.be.ok;
|
||||
|
||||
Ember.run(function () {
|
||||
component.set('value', 'http://example.com/go/docs');
|
||||
});
|
||||
|
||||
expect(component.get('isRelative')).to.not.be.ok;
|
||||
});
|
||||
|
||||
it('identifies a URL as the base URL', function () {
|
||||
var component = this.subject({
|
||||
baseUrl: 'http://example.com/'
|
||||
|
|
212
tests/unit/controllers/settings/navigation-test.js
Normal file
212
tests/unit/controllers/settings/navigation-test.js
Normal file
|
@ -0,0 +1,212 @@
|
|||
/* jshint expr:true */
|
||||
import { expect, assert } from 'chai';
|
||||
import { describeModule, it } from 'ember-mocha';
|
||||
import Ember from 'ember';
|
||||
import { NavItem } from 'ghost/controllers/settings/navigation';
|
||||
|
||||
const { run } = Ember;
|
||||
|
||||
var navSettingJSON = `[
|
||||
{"label":"Home","url":"/"},
|
||||
{"label":"JS Test","url":"javascript:alert('hello');"},
|
||||
{"label":"About","url":"/about"},
|
||||
{"label":"Sub Folder","url":"/blah/blah"},
|
||||
{"label":"Telephone","url":"tel:01234-567890"},
|
||||
{"label":"Mailto","url":"mailto:test@example.com"},
|
||||
{"label":"External","url":"https://example.com/testing?query=test#anchor"},
|
||||
{"label":"No Protocol","url":"//example.com"}
|
||||
]`;
|
||||
|
||||
describeModule(
|
||||
'controller:settings/navigation',
|
||||
'Unit : Controller : settings/navigation',
|
||||
{
|
||||
// Specify the other units that are required for this test.
|
||||
needs: ['service:config', 'service:notifications']
|
||||
},
|
||||
function () {
|
||||
it('blogUrl: captures config and ensures trailing slash', function () {
|
||||
var ctrl = this.subject();
|
||||
ctrl.set('config.blogUrl', 'http://localhost:2368/blog');
|
||||
expect(ctrl.get('blogUrl')).to.equal('http://localhost:2368/blog/');
|
||||
});
|
||||
|
||||
it('navigationItems: generates list of NavItems', function () {
|
||||
var ctrl = this.subject(),
|
||||
lastItem;
|
||||
|
||||
run(() => {
|
||||
ctrl.set('model', Ember.Object.create({navigation: navSettingJSON}));
|
||||
expect(ctrl.get('navigationItems.length')).to.equal(9);
|
||||
expect(ctrl.get('navigationItems.firstObject.label')).to.equal('Home');
|
||||
expect(ctrl.get('navigationItems.firstObject.url')).to.equal('/');
|
||||
expect(ctrl.get('navigationItems.firstObject.last')).to.be.false;
|
||||
|
||||
// adds a blank item as last one is complete
|
||||
lastItem = ctrl.get('navigationItems.lastObject');
|
||||
expect(lastItem.get('label')).to.equal('');
|
||||
expect(lastItem.get('url')).to.equal('');
|
||||
expect(lastItem.get('last')).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('navigationItems: adds blank item if navigation setting is empty', function () {
|
||||
var ctrl = this.subject(),
|
||||
lastItem;
|
||||
|
||||
run(() => {
|
||||
ctrl.set('model', Ember.Object.create({navigation: null}));
|
||||
expect(ctrl.get('navigationItems.length')).to.equal(1);
|
||||
|
||||
lastItem = ctrl.get('navigationItems.lastObject');
|
||||
expect(lastItem.get('label')).to.equal('');
|
||||
expect(lastItem.get('url')).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
it('updateLastNavItem: correctly sets "last" properties', function () {
|
||||
var ctrl = this.subject(),
|
||||
item1,
|
||||
item2;
|
||||
|
||||
run(() => {
|
||||
ctrl.set('model', Ember.Object.create({navigation: navSettingJSON}));
|
||||
|
||||
item1 = ctrl.get('navigationItems.lastObject');
|
||||
expect(item1.get('last')).to.be.true;
|
||||
|
||||
ctrl.get('navigationItems').addObject(Ember.Object.create({label: 'Test', url: '/test'}));
|
||||
|
||||
item2 = ctrl.get('navigationItems.lastObject');
|
||||
expect(item2.get('last')).to.be.true;
|
||||
expect(item1.get('last')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
it('save: validates nav items', function (done) {
|
||||
var ctrl = this.subject();
|
||||
|
||||
run(() => {
|
||||
ctrl.set('model', Ember.Object.create({navigation: `[
|
||||
{"label":"First", "url":"/"},
|
||||
{"label":"", "url":"/second"},
|
||||
{"label":"Third", "url":""}
|
||||
]`}));
|
||||
// blank item won't get added because the last item is incomplete
|
||||
expect(ctrl.get('navigationItems.length')).to.equal(3);
|
||||
|
||||
ctrl.save().then(function passedValidation() {
|
||||
assert(false, 'navigationItems weren\'t validated on save');
|
||||
done();
|
||||
}).catch(function failedValidation() {
|
||||
let navItems = ctrl.get('navigationItems');
|
||||
expect(navItems[0].get('errors')).to.be.empty;
|
||||
expect(navItems[1].get('errors.firstObject.attribute')).to.equal('label');
|
||||
expect(navItems[2].get('errors.firstObject.attribute')).to.equal('url');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('save: generates new navigation JSON', function (done) {
|
||||
var ctrl = this.subject(),
|
||||
model = Ember.Object.create({navigation: {}}),
|
||||
expectedJSON = `[{"label":"New","url":"/new"}]`;
|
||||
|
||||
model.save = function () {
|
||||
var self = this;
|
||||
return new Ember.RSVP.Promise(function (resolve, reject) {
|
||||
return resolve(self);
|
||||
});
|
||||
};
|
||||
|
||||
run(() => {
|
||||
ctrl.set('model', model);
|
||||
|
||||
// remove inserted blank item so validation works
|
||||
ctrl.get('navigationItems').removeObject(ctrl.get('navigationItems.firstObject'));
|
||||
// add new object
|
||||
ctrl.get('navigationItems').addObject(NavItem.create({label:'New', url:'/new'}));
|
||||
|
||||
ctrl.save().then(function success() {
|
||||
expect(ctrl.get('model.navigation')).to.equal(expectedJSON);
|
||||
done();
|
||||
}, function failure() {
|
||||
assert(false, 'save failed with valid data');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('action - addItem: adds item to navigationItems', function () {
|
||||
var ctrl = this.subject();
|
||||
|
||||
run(() => {
|
||||
ctrl.set('navigationItems', [NavItem.create({label: 'First', url: '/first', last: true})]);
|
||||
expect(ctrl.get('navigationItems.length')).to.equal(1);
|
||||
ctrl.send('addItem');
|
||||
expect(ctrl.get('navigationItems.length')).to.equal(2);
|
||||
expect(ctrl.get('navigationItems.firstObject.last')).to.be.false;
|
||||
expect(ctrl.get('navigationItems.lastObject.label')).to.equal('');
|
||||
expect(ctrl.get('navigationItems.lastObject.url')).to.equal('');
|
||||
expect(ctrl.get('navigationItems.lastObject.last')).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('action - addItem: doesn\'t insert new item if last object is incomplete', function () {
|
||||
var ctrl = this.subject();
|
||||
|
||||
run(() => {
|
||||
ctrl.set('navigationItems', [NavItem.create({label: '', url: '', last: true})]);
|
||||
expect(ctrl.get('navigationItems.length')).to.equal(1);
|
||||
ctrl.send('addItem');
|
||||
expect(ctrl.get('navigationItems.length')).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('action - deleteItem: removes item from navigationItems', function () {
|
||||
var ctrl = this.subject(),
|
||||
navItems = [
|
||||
NavItem.create({label: 'First', url: '/first'}),
|
||||
NavItem.create({label: 'Second', url: '/second', last: true})
|
||||
];
|
||||
|
||||
run(() => {
|
||||
ctrl.set('navigationItems', navItems);
|
||||
expect(ctrl.get('navigationItems').mapBy('label')).to.deep.equal(['First', 'Second']);
|
||||
ctrl.send('deleteItem', ctrl.get('navigationItems.firstObject'));
|
||||
expect(ctrl.get('navigationItems').mapBy('label')).to.deep.equal(['Second']);
|
||||
});
|
||||
});
|
||||
|
||||
it('action - moveItem: updates navigationItems list', function () {
|
||||
var ctrl = this.subject(),
|
||||
navItems = [
|
||||
NavItem.create({label: 'First', url: '/first'}),
|
||||
NavItem.create({label: 'Second', url: '/second', last: true})
|
||||
];
|
||||
|
||||
run(() => {
|
||||
ctrl.set('navigationItems', navItems);
|
||||
expect(ctrl.get('navigationItems').mapBy('label')).to.deep.equal(['First', 'Second']);
|
||||
ctrl.send('moveItem', 1, 0);
|
||||
expect(ctrl.get('navigationItems').mapBy('label')).to.deep.equal(['Second', 'First']);
|
||||
});
|
||||
});
|
||||
|
||||
it('action - updateUrl: updates URL on navigationItem', function () {
|
||||
var ctrl = this.subject(),
|
||||
navItems = [
|
||||
NavItem.create({label: 'First', url: '/first'}),
|
||||
NavItem.create({label: 'Second', url: '/second', last: true})
|
||||
];
|
||||
|
||||
run(() => {
|
||||
ctrl.set('navigationItems', navItems);
|
||||
expect(ctrl.get('navigationItems').mapBy('url')).to.deep.equal(['/first', '/second']);
|
||||
ctrl.send('updateUrl', '/new', ctrl.get('navigationItems.firstObject'));
|
||||
expect(ctrl.get('navigationItems').mapBy('url')).to.deep.equal(['/new', '/second']);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
120
tests/unit/validators/nav-item-test.js
Normal file
120
tests/unit/validators/nav-item-test.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describe,
|
||||
it
|
||||
} from 'mocha';
|
||||
import validator from 'ghost/validators/nav-item';
|
||||
import { NavItem } from 'ghost/controllers/settings/navigation';
|
||||
|
||||
var testInvalidUrl,
|
||||
testValidUrl;
|
||||
|
||||
testInvalidUrl = function (url) {
|
||||
let navItem = NavItem.create({url: url});
|
||||
|
||||
validator.check(navItem, 'url');
|
||||
|
||||
expect(validator.get('passed'), `"${url}" passed`).to.be.false;
|
||||
expect(navItem.get('errors').errorsFor('url')).to.deep.equal([{
|
||||
attribute: 'url',
|
||||
message: 'You must specify a valid URL or relative path'
|
||||
}]);
|
||||
expect(navItem.get('hasValidated')).to.include('url');
|
||||
};
|
||||
|
||||
testValidUrl = function (url) {
|
||||
let navItem = NavItem.create({url: url});
|
||||
|
||||
validator.check(navItem, 'url');
|
||||
|
||||
expect(validator.get('passed'), `"${url}" failed`).to.be.true;
|
||||
expect(navItem.get('hasValidated')).to.include('url');
|
||||
};
|
||||
|
||||
describe('Unit : Validator : nav-item', function () {
|
||||
it('requires label presence', function () {
|
||||
let navItem = NavItem.create();
|
||||
|
||||
validator.check(navItem, 'label');
|
||||
|
||||
expect(validator.get('passed')).to.be.false;
|
||||
expect(navItem.get('errors').errorsFor('label')).to.deep.equal([{
|
||||
attribute: 'label',
|
||||
message: 'You must specify a label'
|
||||
}]);
|
||||
expect(navItem.get('hasValidated')).to.include('label');
|
||||
});
|
||||
|
||||
it('doesn\'t validate label if empty and last', function () {
|
||||
let navItem = NavItem.create({last: true});
|
||||
|
||||
validator.check(navItem, 'label');
|
||||
|
||||
expect(validator.get('passed')).to.be.true;
|
||||
});
|
||||
|
||||
it('requires url presence', function () {
|
||||
let navItem = NavItem.create();
|
||||
|
||||
validator.check(navItem, 'url');
|
||||
|
||||
expect(validator.get('passed')).to.be.false;
|
||||
expect(navItem.get('errors').errorsFor('url')).to.deep.equal([{
|
||||
attribute: 'url',
|
||||
message: 'You must specify a URL or relative path'
|
||||
}]);
|
||||
expect(navItem.get('hasValidated')).to.include('url');
|
||||
});
|
||||
|
||||
it('fails on invalid url values', function () {
|
||||
let invalidUrls = [
|
||||
'test@example.com',
|
||||
'/has spaces',
|
||||
'no-leading-slash',
|
||||
'http://example.com/with spaces'
|
||||
];
|
||||
|
||||
invalidUrls.forEach(function (url) {
|
||||
testInvalidUrl(url);
|
||||
});
|
||||
});
|
||||
|
||||
it('passes on valid url values', function () {
|
||||
let validUrls = [
|
||||
'http://localhost:2368',
|
||||
'http://localhost:2368/some-path',
|
||||
'https://localhost:2368/some-path',
|
||||
'//localhost:2368/some-path',
|
||||
'http://localhost:2368/#test',
|
||||
'http://localhost:2368/?query=test&another=example',
|
||||
'http://localhost:2368/?query=test&another=example#test',
|
||||
'tel:01234-567890',
|
||||
'mailto:test@example.com',
|
||||
'http://some:user@example.com:1234',
|
||||
'/relative/path'
|
||||
];
|
||||
|
||||
validUrls.forEach(function (url) {
|
||||
testValidUrl(url);
|
||||
});
|
||||
});
|
||||
|
||||
it('doesn\'t validate url if empty and last', function () {
|
||||
let navItem = NavItem.create({last: true});
|
||||
|
||||
validator.check(navItem, 'url');
|
||||
|
||||
expect(validator.get('passed')).to.be.true;
|
||||
});
|
||||
|
||||
it('validates url and label by default', function () {
|
||||
let navItem = NavItem.create();
|
||||
|
||||
validator.check(navItem);
|
||||
|
||||
expect(navItem.get('errors').errorsFor('label')).to.not.be.empty;
|
||||
expect(navItem.get('errors').errorsFor('url')).to.not.be.empty;
|
||||
expect(validator.get('passed')).to.be.false;
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue