1
0
Fork 0
mirror of https://github.com/TryGhost/Ghost-Admin.git synced 2023-12-14 02:33:04 +01:00

Migrate to latest ember, ember-mocha and modern ember testing (#1044)

no issue
- upgrade to latest `ember-source` and related dependencies including `ember-cli`
- upgrade to latest `ember-mocha` and modern ember testing setup
    - https://github.com/emberjs/rfcs/blob/master/text/0268-acceptance-testing-refactor.md
    - switch from using global acceptance test helpers and `native-dom-helpers` to using the new `ember-test-helpers` methods
    - use [`chai-dom`](https://github.com/nathanboktae/chai-dom) assertions where in some places (still a lot of places in the tests that could use these)
- pin `ember-in-viewport` to 3.0.x to work around incompatibilities between different versions used in `ember-light-table`, `ember-infinity`, and `ember-sticky-element`
    - incompatibilities manifested as "Invalid value used as weak map key" errors thrown when using `ember-light-table` (subscribers screen)
- pin `ember-power-datepicker` to unreleased version that contains a move from global acceptance test helpers to modern test helpers
This commit is contained in:
Kevin Ansfield 2019-01-02 09:58:55 +00:00 committed by GitHub
parent 7459decbfc
commit 079c8ccc2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
133 changed files with 5507 additions and 5392 deletions

18
.eslintignore Normal file
View file

@ -0,0 +1,18 @@
# unconventional js
/blueprints/*/files/
/vendor/
# compiled output
/dist/
/tmp/
# dependencies
/bower_components/
# misc
/coverage/
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try

4
.gitignore vendored
View file

@ -46,8 +46,8 @@ Session.vim
# misc
/connect.lock
/coverage/*
/coverage/
/libpeerconnection.log
testem.log
/testem.log
/concat-stats-for
jsconfig.json

View file

@ -32,8 +32,8 @@ const CmEditorComponent = Component.extend({
_value: boundOneWay('value'), // make sure a value exists
didReceiveAttrs() {
if (this.get('value') === null || undefined) {
this.set('value', '');
if (this.get('_value') === null || undefined) {
this.set('_value', '');
}
},
@ -69,9 +69,7 @@ const CmEditorComponent = Component.extend({
loader.loadScript('codemirror', 'assets/codemirror/codemirror.js')
]);
scheduleOnce('afterRender', this, function () {
this._initCodeMirror();
});
scheduleOnce('afterRender', this, this._initCodeMirror);
}),
_initCodeMirror() {

View file

@ -0,0 +1,6 @@
import InfinityLoader from 'ember-infinity/components/infinity-loader';
import layout from '../templates/components/infinity-loader';
export default InfinityLoader.extend({
layout
});

View file

@ -27,30 +27,24 @@ export default TextField.extend({
classNames: 'gh-input',
// Allowed actions
clearErrors: () => {},
update() {},
clearErrors() {},
isBaseUrl: computed('baseUrl', 'value', function () {
return this.get('baseUrl') === this.get('value');
return this.baseUrl === this.value;
}),
didReceiveAttrs() {
this._super(...arguments);
let baseUrl = this.get('baseUrl');
let url = this.get('url');
// if we have a relative url, create the absolute url to be displayed in the input
if (isRelative(url)) {
url = joinUrlParts(baseUrl, url);
}
this.set('value', url);
// value coming is likely to be relative but we always want to show
// absolute urls in the input fields
this.set('value', this._makeAbsoluteUrl(this.url));
},
focusIn(event) {
this.set('hasFocus', true);
if (this.get('isBaseUrl')) {
if (this.isBaseUrl) {
// position the cursor at the end of the input
run.next(function (el) {
let {length} = el.value;
@ -62,7 +56,7 @@ export default TextField.extend({
keyDown(event) {
// delete the "placeholder" value all at once
if (this.get('isBaseUrl') && (event.keyCode === 8 || event.keyCode === 46)) {
if (this.isBaseUrl && (event.keyCode === 8 || event.keyCode === 46)) {
this.set('value', '');
event.preventDefault();
@ -92,9 +86,9 @@ export default TextField.extend({
},
notifyUrlChanged() {
let url = this.get('value').trim();
let url = this.value.trim();
let urlURI = URI.parse(url);
let baseUrl = this.get('baseUrl');
let baseUrl = this.baseUrl;
let baseURI = URI.parse(baseUrl);
function getHost(uri) {
@ -115,18 +109,12 @@ export default TextField.extend({
// 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 (isRelative(url)) {
url = joinUrlParts(baseUrl, url);
url = this.update(`mailto:${url}`);
this.set('value', url);
return;
}
// get our baseUrl relativity checks in order
let isOnSameHost = urlHost === baseHost;
let isAnchorLink = url.match(/^#/);
let isRelativeToBasePath = urlURI.getPath() && urlURI.getPath().indexOf(baseURI.getPath()) === 0;
@ -135,6 +123,8 @@ export default TextField.extend({
isRelativeToBasePath = true;
}
let isOnSameHost = urlHost === baseHost || (!urlHost && isRelativeToBasePath);
// if relative to baseUrl, remove the base url before sending to action
if (!isAnchorLink && isOnSameHost && isRelativeToBasePath) {
url = url.replace(/^[a-zA-Z0-9-]+:/, '');
@ -147,7 +137,7 @@ export default TextField.extend({
url = url.replace(baseURI.getPath().slice(0, -1), '');
}
if (url !== '' || !this.get('isNew')) {
if (url !== '' || !this.isNew) {
if (!url.match(/^\//)) {
url = `/${url}`;
}
@ -158,9 +148,19 @@ export default TextField.extend({
}
}
let action = this.get('update');
if (action) {
action(url);
// we update with the relative URL but then transform it back to absolute
// for the input value. This avoids problems where the underlying relative
// value hasn't changed even though the input value has
if (url.match(/^(\/\/|#|[a-zA-Z0-9-]+:)/) || validator.isURL(url) || validator.isURL(`${baseHost}${url}`)) {
url = this.update(url);
this.set('value', this._makeAbsoluteUrl(url));
}
},
_makeAbsoluteUrl(url) {
if (isRelative(url)) {
url = joinUrlParts(this.baseUrl, url);
}
return url;
}
});

View file

@ -10,6 +10,12 @@ export default Component.extend(ValidationState, {
new: false,
// closure actions
addItem() {},
deleteItem() {},
updateUrl() {},
updateLabel() {},
errors: readOnly('navItem.errors'),
errorClass: computed('hasError', function () {
@ -20,31 +26,19 @@ export default Component.extend(ValidationState, {
actions: {
addItem() {
let action = this.get('addItem');
if (action) {
action();
}
this.addItem();
},
deleteItem(item) {
let action = this.get('deleteItem');
if (action) {
action(item);
}
this.deleteItem(item);
},
updateUrl(value) {
let action = this.get('updateUrl');
if (action) {
action(value, this.get('navItem'));
}
return this.updateUrl(value, this.navItem);
},
updateLabel(value) {
let action = this.get('updateLabel');
if (action) {
action(value, this.get('navItem'));
}
return this.updateLabel(value, this.navItem);
},
clearLabelErrors() {

View file

@ -15,7 +15,8 @@ export default Component.extend({
_availableTags: null,
availableTags: sort('_availableTags.[]', function (tagA, tagB) {
return tagA.name.localeCompare(tagB.name);
// ignorePunctuation means the # in internal tag names is ignored
return tagA.name.localeCompare(tagB.name, undefined, {ignorePunctuation: true});
}),
availableTagNames: computed('availableTags.@each.name', function () {

View file

@ -1,4 +1,3 @@
import $ from 'jquery';
import Component from '@ember/component';
import {htmlSafe} from '@ember/string';
@ -15,21 +14,25 @@ export default Component.extend({
href: htmlSafe('javascript:;'),
click() {
let anchor = this.get('anchor');
let $el = $(anchor);
let el = document.querySelector(this.anchor);
if ($el) {
if (el) {
// Scrolls to the top of main content or whatever
// is passed to the anchor attribute
$('body').scrollTop($el.offset().top);
document.body.scrollTop = el.getBoundingClientRect().top;
let removeTabindex = function () {
el.removeAttribute('tabindex');
};
// This sets focus on the content which was skipped to
// upon losing focus, the tabindex should be removed
// so that normal keyboard navigation picks up from focused
// element
$($el).attr('tabindex', -1).on('blur focusout', function () {
$(this).removeAttr('tabindex');
}).focus();
el.setAttribute('tabindex', -1);
el.focus();
el.addEventListener('blur', removeTabindex);
el.addEventListener('focusout', removeTabindex);
}
}
});

View file

@ -0,0 +1,6 @@
import InfinityLoader from 'ember-infinity/components/infinity-loader';
import layout from '../templates/components/gh-infinity-loader';
export default InfinityLoader.extend({
layout
});

View file

@ -5,7 +5,9 @@ export default ModalComponent.extend({
actions: {
confirm() {
this.confirm().finally(() => {
if (!this.isDestroyed && !this.isDestroying) {
this.send('closeModal');
}
});
}
},

View file

@ -84,7 +84,7 @@ export default Controller.extend({
availableTags: computed('_availableTags.[]', function () {
let tags = this.get('_availableTags')
.filter(tag => tag.get('id') !== null)
.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name));
.sort((tagA, tagB) => tagA.name.localeCompare(tagB.name, undefined, {ignorePunctuation: true}));
let options = tags.toArray();
options.unshiftObject({name: 'All tags', slug: null});

View file

@ -81,6 +81,8 @@ export default Controller.extend({
navItem.set('url', url);
this.set('dirtyAttributes', true);
return url;
},
toggleLeaveSettingsModal(transition) {

View file

@ -19,7 +19,8 @@ export default Controller.extend({
// tags are sorted by name
sortedTags: sort('filteredTags', function (tagA, tagB) {
return tagA.name.localeCompare(tagB.name);
// ignorePunctuation means the # in internal tag names is ignored
return tagA.name.localeCompare(tagB.name, undefined, {ignorePunctuation: true});
}),
actions: {

View file

@ -45,7 +45,9 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, ShortcutsRoute, {
deactivate() {
this._super(...arguments);
if (!this.isDestroyed && !this.isDestroying) {
this.send('resetShortcutsScope');
}
},
actions: {

View file

@ -19,8 +19,14 @@ import {
// specificity to re-use keys for i18n lookups
export default Service.extend({
delayedNotifications: emberA(),
content: emberA(),
delayedNotifications: null,
content: null,
init() {
this._super(...arguments);
this.delayedNotifications = emberA();
this.content = emberA();
},
upgradeStatus: service(),

View file

@ -5,11 +5,11 @@ import {inject as service} from '@ember/service';
export default SessionService.extend({
feature: service(),
store: service(),
dataStore: service('store'), // SessionService.store already exists
tour: service(),
user: computed(function () {
return this.get('store').queryRecord('user', {id: 'me'});
return this.get('dataStore').queryRecord('user', {id: 'me'});
}),
authenticate() {

View file

@ -11,9 +11,11 @@
}}
{{/if}}
{{#gh-main onMouseEnter=(action "closeAutoNav" target=ui)}}
{{!-- {{#gh-main onMouseEnter=(action "closeAutoNav" target=ui)}} --}}
<main class="gh-main" role="main">
{{outlet}}
{{/gh-main}}
</main>
{{!-- {{/gh-main}} --}}
{{gh-notifications}}

View file

@ -1,7 +1,7 @@
{{#if hasBlock}}
{{yield}}
{{yield infinityModelContent}}
{{else}}
{{#if infinityModel.reachedInfinity}}
{{#if isDoneLoading}}
{{else}}
<div class="gh-loading-spinner"></div>
{{/if}}

View file

@ -14,12 +14,20 @@
input=(action (mut navItem.label) value="target.value")
keyPress=(action "clearLabelErrors")
focus-out=(action "updateLabel" navItem.label)
data-test-input="label"
}}
{{gh-error-message errors=navItem.errors property="label"}}
{{gh-error-message errors=navItem.errors property="label" data-test-error="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 isNew=navItem.isNew update=(action "updateUrl") clearErrors=(action "clearUrlErrors")}}
{{gh-error-message errors=navItem.errors property="url"}}
{{gh-navitem-url-input
baseUrl=baseUrl
isNew=navItem.isNew
url=(readonly navItem.url)
update=(action "updateUrl")
clearErrors=(action "clearUrlErrors")
data-test-input="url"
}}
{{gh-error-message errors=navItem.errors property="url" data-test-error="url"}}
{{/gh-validation-status-container}}
</div>

View file

@ -1,4 +1,4 @@
<header class="modal-header">
<header class="modal-header" data-test-modal="delete-subscriber">
<h1>Are you sure?</h1>
</header>
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
@ -8,6 +8,12 @@
</div>
<div class="modal-footer">
<button {{action "closeModal"}} class="gh-btn"><span>Cancel</span></button>
{{gh-task-button "Delete" successText="Deleted" task=deleteSubscriber class="gh-btn gh-btn-red gh-btn-icon"}}
<button {{action "closeModal"}} class="gh-btn" data-test-button="cancel-delete-subscriber">
<span>Cancel</span>
</button>
{{gh-task-button "Delete"
successText="Deleted"
task=deleteSubscriber
class="gh-btn gh-btn-red gh-btn-icon"
data-test-button="confirm-delete-subscriber"}}
</div>

View file

@ -1,4 +1,4 @@
<header class="modal-header">
<header class="modal-header" data-test-modal="delete-user">
<h1>Are you sure you want to delete this user?</h1>
</header>
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
@ -8,7 +8,7 @@
{{#if user.count.posts}}
<ul>
<li>The user will not have access to this blog anymore</li>
<li><strong>{{pluralize user.count.posts 'post'}}</strong> created by this user will be deleted</li>
<li><strong data-test-text="user-post-count">{{pluralize user.count.posts 'post'}}</strong> created by this user will be deleted</li>
<li>All other user data will be deleted</li>
</ul>
{{else}}
@ -20,10 +20,12 @@
</div>
<div class="modal-footer">
<button {{action "closeModal"}} class="gh-btn"><span>Cancel</span></button>
{{#if user.count.posts}}
{{gh-task-button "Delete user and their posts" successText="Deleted" task=deleteUser class="gh-btn gh-btn-red gh-btn-icon"}}
{{else}}
{{gh-task-button "Delete user" successText="Deleted" task=deleteUser class="gh-btn gh-btn-red gh-btn-icon"}}
{{/if}}
<button {{action "closeModal"}} class="gh-btn" data-test-button="cancel-delete-user">
<span>Cancel</span>
</button>
{{gh-task-button (if user.count.posts "Delete user and their posts" "Delete user")
successText="Deleted"
task=deleteUser
class="gh-btn gh-btn-red gh-btn-icon"
data-test-button="confirm-delete-user"}}
</div>

View file

@ -1,4 +1,4 @@
<header class="modal-header">
<header class="modal-header" data-test-modal="import-subscribers">
<h1>
{{#if response}}
Import Successful
@ -14,18 +14,18 @@
<table class="subscribers-import-results">
<tr>
<td>Imported:</td>
<td align="left">{{response.imported}}</td>
<td align="left" data-test-text="import-subscribers-imported">{{response.imported}}</td>
</tr>
{{#if response.duplicates}}
<tr>
<td>Duplicates:</td>
<td align="left">{{response.duplicates}}</td>
<td align="left" data-test-text="import-subscribers-duplicates">{{response.duplicates}}</td>
</tr>
{{/if}}
{{#if response.invalid}}
<tr>
<td>Invalid:</td>
<td align="left">{{response.invalid}}</td>
<td align="left" data-test-text="import-subscribers-invalid">{{response.invalid}}</td>
</tr>
{{/if}}
</table>
@ -41,7 +41,7 @@
</div>
<div class="modal-footer">
<button {{action "closeModal"}} disabled={{closeDisabled}} class="gh-btn">
<button {{action "closeModal"}} disabled={{closeDisabled}} class="gh-btn" data-test-button="close-import-subscribers">
<span>{{#if response}}Close{{else}}Cancel{{/if}}</span>
</button>
</div>

View file

@ -33,7 +33,7 @@
{{one-way-select
id="new-user-role"
name="role"
options=roles
options=(readonly roles)
optionValuePath="id"
optionLabelPath="name"
value=role

View file

@ -1,4 +1,4 @@
<header class="modal-header" data-modal="unsaved-settings">
<header class="modal-header" data-test-modal="unsaved-settings">
<h1>Are you sure you want to leave this page?</h1>
</header>
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>

View file

@ -1,4 +1,4 @@
<header class="modal-header">
<header class="modal-header" data-test-modal="new-subscriber">
<h1>Add a Subscriber</h1>
</header>
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
@ -16,14 +16,21 @@
name="email"
autofocus="autofocus"
autocapitalize="off"
autocorrect="off">
{{gh-error-message errors=subscriber.errors property="email"}}
autocorrect="off"
data-test-input="new-subscriber-email">
{{gh-error-message errors=subscriber.errors property="email" data-test-error="new-subscriber-email"}}
{{/gh-form-group}}
</fieldset>
</div>
<div class="modal-footer">
<button {{action "closeModal"}} class="gh-btn"><span>Cancel</span></button>
{{gh-task-button "Add" successText="Added" task=addSubscriber class="gh-btn gh-btn-green gh-btn-icon"}}
<button {{action "closeModal"}} class="gh-btn" data-test-button="cancel-new-subscriber">
<span>Cancel</span>
</button>
{{gh-task-button "Add"
successText="Added"
task=addSubscriber
class="gh-btn gh-btn-green gh-btn-icon"
data-test-button="create-subscriber"}}
</div>

View file

@ -108,7 +108,7 @@
{{/each}}
</ol>
{{infinity-loader
{{gh-infinity-loader
infinityModel=postsInfinityModel
scrollable=".gh-main"
triggerOffset=1000}}

View file

@ -19,13 +19,25 @@
<div class="gh-blognav-container">
<form id="settings-navigation" class="gh-blognav" novalidate="novalidate">
{{#sortable-objects sortableObjectList=settings.navigation useSwap=false}}
{{#each settings.navigation as |navItem|}}
{{#each settings.navigation as |navItem index|}}
{{#draggable-object content=navItem dragHandle=".gh-blognav-grab" isSortable=true}}
{{gh-navitem navItem=navItem baseUrl=blogUrl addItem=(action "addNavItem") deleteItem=(action "deleteNavItem") updateUrl=(action "updateUrl") updateLabel=(action "updateLabel")}}
{{gh-navitem
navItem=navItem
baseUrl=blogUrl
addItem=(action "addNavItem")
deleteItem=(action "deleteNavItem")
updateUrl=(action "updateUrl")
updateLabel=(action "updateLabel")
data-test-navitem=index}}
{{/draggable-object}}
{{/each}}
{{/sortable-objects}}
{{gh-navitem navItem=newNavItem baseUrl=blogUrl addItem=(action "addNavItem") updateUrl=(action "updateUrl")}}
{{gh-navitem
navItem=newNavItem
baseUrl=blogUrl
addItem=(action "addNavItem")
updateUrl=(action "updateUrl")
data-test-navitem="new"}}
</form>
</div>

View file

@ -51,7 +51,7 @@
{{#unless slackSettings.errors.url}}
<p>Set up a new incoming webhook <a href="https://my.slack.com/apps/new/A0F7XDUAZ-incoming-webhooks" target="_blank">here</a>, and grab the URL.</p>
{{else}}
{{gh-error-message errors=slackSettings.errors property="url"}}
{{gh-error-message errors=slackSettings.errors property="url" data-test-error="slack-url"}}
{{/unless}}
{{/gh-form-group}}
</div>

View file

@ -92,7 +92,7 @@
</div>
</section>
{{infinity-loader
{{gh-infinity-loader
infinityModel=activeUsers
scrollable=".gh-main"
triggerOffset=500}}

View file

@ -136,7 +136,7 @@
data-test-name-input=true
}}
{{#if user.errors.name}}
{{gh-error-message errors=user.errors property="name"}}
{{gh-error-message errors=user.errors property="name" data-test-error="user-name"}}
{{else}}
<p>Use your real name so people can recognise you</p>
{{/if}}
@ -157,7 +157,7 @@
data-test-slug-input=true
}}
<p>{{gh-blog-url}}/author/{{slugValue}}</p>
{{gh-error-message errors=user.errors property="slug"}}
{{gh-error-message errors=user.errors property="slug" data-test-error="user-slug"}}
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="email"}}
@ -176,7 +176,7 @@
input=(action (mut user.email) value="target.value")
focus-out=(action "validate" "email" target=user)
data-test-email-input=true}}
{{gh-error-message errors=user.errors property="email"}}
{{gh-error-message errors=user.errors property="email" data-test-error="user-email"}}
{{else}}
<span>{{user.email}}</span>
{{/if}}
@ -208,7 +208,7 @@
input=(action (mut user.location) value="target.value")
focus-out=(action "validate" "location" target=user)
data-test-location-input=true}}
{{gh-error-message errors=user.errors property="location" data-test-location-error=true}}
{{gh-error-message errors=user.errors property="location" data-test-error="user-location"}}
<p>Where in the world do you live?</p>
{{/gh-form-group}}
@ -224,7 +224,7 @@
input=(action (mut user.website) value="target.value")
focus-out=(action "validate" "website" target=user)
data-test-website-input=true}}
{{gh-error-message errors=user.errors property="website" data-test-website-error=true}}
{{gh-error-message errors=user.errors property="website" data-test-error="user-website"}}
<p>Have a website or blog other than this one? Link it!</p>
{{/gh-form-group}}
@ -241,7 +241,7 @@
focus-out=(action "validateFacebookUrl")
data-test-facebook-input=true
}}
{{gh-error-message errors=user.errors property="facebook" data-test-facebook-error=true}}
{{gh-error-message errors=user.errors property="facebook" data-test-error="user-facebook"}}
<p>URL of your personal Facebook Profile</p>
{{/gh-form-group}}
@ -258,7 +258,7 @@
focus-out=(action "validateTwitterUrl")
data-test-twitter-input=true
}}
{{gh-error-message errors=user.errors property="twitter" data-test-twitter-error=true}}
{{gh-error-message errors=user.errors property="twitter" data-test-error="user-twitter"}}
<p>URL of your personal Twitter profile</p>
{{/gh-form-group}}
@ -271,7 +271,7 @@
focus-out=(action "validate" "bio" target=user)
data-test-bio-input=true
}}
{{gh-error-message errors=user.errors property="bio" data-test-bio-error=true}}
{{gh-error-message errors=user.errors property="bio" data-test-error="user-bio"}}
<p>
Write about you, in 200 characters or less.
{{gh-count-characters user.bio}}
@ -284,7 +284,7 @@
</form> {{! user details form }}
{{!-- If an administrator is viewing Owner's profile or we're using Ghost.org OAuth then hide inputs for change password --}}
{{!-- If an administrator is viewing Owner's profile then hide inputs for change password --}}
{{#if canChangePassword}}
<form id="password-reset" class="user-profile" novalidate="novalidate" autocomplete="off" {{action (perform user.saveNewPassword) on="submit"}}>
<fieldset>
@ -301,7 +301,7 @@
)
data-test-old-pass-input=true
}}
{{gh-error-message errors=user.errors property="password"}}
{{gh-error-message errors=user.errors property="password" data-test-error="user-old-pass"}}
{{/gh-form-group}}
{{/unless}}
@ -317,7 +317,7 @@
)
data-test-new-pass-input=true
}}
{{gh-error-message errors=user.errors property="newPassword"}}
{{gh-error-message errors=user.errors property="newPassword" data-test-error="user-new-pass"}}
{{/gh-form-group}}
{{#gh-form-group errors=user.errors hasValidated=user.hasValidated property="ne2Password"}}
@ -332,7 +332,7 @@
)
data-test-ne2-pass-input=true
}}
{{gh-error-message errors=user.errors property="ne2Password"}}
{{gh-error-message errors=user.errors property="ne2Password" data-test-error="user-ne2-pass"}}
{{/gh-form-group}}
<div class="form-group">

View file

@ -66,6 +66,7 @@ module.exports = function (environment) {
ENV.APP.LOG_VIEW_LOOKUPS = false;
ENV.APP.rootElement = '#ember-testing';
ENV.APP.autoboot = false;
// This is needed so that browserify dependencies in tests work correctly
// See https://github.com/ef4/ember-browserify/issues/14

View file

@ -1,3 +1,4 @@
{
"application-template-wrapper": false
"application-template-wrapper": false,
"jquery-integration": true
}

View file

@ -12,6 +12,6 @@
],
"dependencies": {
"ember-cli-babel": "^6.11.0",
"ember-cli-htmlbars": "^2.0.3"
"ember-cli-htmlbars": "^3.0.1"
}
}

View file

@ -19,24 +19,24 @@
"start": "ember serve",
"build": "ember build",
"test": "ember test",
"lint:js": "eslint ./*.js app config lib mirage server tests",
"lint:js": "eslint .",
"coverage": "cat ./coverage/lcov.info | coveralls"
},
"engines": {
"node": ">= 6"
},
"devDependencies": {
"@ember/optional-features": "0.6.1",
"@ember/optional-features": "0.6.4",
"@html-next/vertical-collection": "1.0.0-beta.12",
"@tryghost/mobiledoc-kit": "0.11.1-ghost.5",
"blueimp-md5": "2.10.0",
"broccoli-asset-rev": "2.7.0",
"broccoli-clean-css": "^2.0.1",
"broccoli-concat": "3.2.2",
"broccoli-concat": "3.7.3",
"broccoli-funnel": "2.0.1",
"broccoli-merge-trees": "3.0.0",
"broccoli-merge-trees": "3.0.1",
"broccoli-uglify-sourcemap": "2.2.0",
"chai-jquery": "2.0.0",
"chai-dom": "1.8.1",
"codemirror": "5.39.0",
"coveralls": "3.0.2",
"csscomb": "4.2.0",
@ -45,21 +45,20 @@
"ember-ajax": "3.1.1",
"ember-assign-helper": "0.1.2",
"ember-browserify": "1.2.2",
"ember-cli": "3.1.4",
"ember-cli": "3.4.1",
"ember-cli-app-version": "3.2.0",
"ember-cli-babel": "6.15.0",
"ember-cli-babel": "6.16.0",
"ember-cli-chai": "0.5.0",
"ember-cli-cjs-transform": "1.3.0",
"ember-cli-code-coverage": "0.4.2",
"ember-cli-dependency-checker": "2.1.1",
"ember-cli-dependency-checker": "3.0.0",
"ember-cli-es6-transform": "^0.0.3",
"ember-cli-eslint": "4.2.3",
"ember-cli-ghost-spirit": "0.0.6",
"ember-cli-htmlbars": "2.0.3",
"ember-cli-htmlbars-inline-precompile": "1.0.3",
"ember-cli-htmlbars": "3.0.1",
"ember-cli-htmlbars-inline-precompile": "1.0.5",
"ember-cli-inject-live-reload": "1.7.0",
"ember-cli-mirage": "0.4.7",
"ember-cli-mocha": "^0.15.0",
"ember-cli-mirage": "0.4.9",
"ember-cli-moment-shim": "3.7.1",
"ember-cli-node-assets": "0.2.2",
"ember-cli-postcss": "4.0.0",
@ -69,28 +68,30 @@
"ember-cli-test-loader": "2.2.0",
"ember-cli-uglify": "2.1.0",
"ember-composable-helpers": "2.1.0",
"ember-concurrency": "0.8.19",
"ember-data": "3.2.0",
"ember-concurrency": "0.8.22",
"ember-data": "3.4.0",
"ember-drag-drop": "0.4.8",
"ember-export-application-global": "2.0.0",
"ember-fetch": "5.0.0",
"ember-in-viewport": "3.0.3",
"ember-infinity": "1.0.1",
"ember-light-table": "1.13.1",
"ember-fetch": "5.1.3",
"ember-in-viewport": "~3.0.3",
"ember-infinity": "1.2.6",
"ember-light-table": "1.13.2",
"ember-load": "0.0.12",
"ember-load-initializers": "1.1.0",
"ember-native-dom-helpers": "0.6.2",
"ember-mocha": "0.14.0",
"ember-moment": "7.8.0",
"ember-one-way-select": "4.0.0",
"ember-power-datepicker": "0.4.0",
"ember-power-select": "1.10.4",
"ember-resolver": "4.5.6",
"ember-power-calendar-moment": "0.1.3",
"ember-power-datepicker": "https://github.com/cibernox/ember-power-datepicker/tarball/cd4dffa8852236a3312f6dc7040719c0e9196bc0",
"ember-power-select": "2.0.9",
"ember-resolver": "5.0.1",
"ember-responsive": "2.0.5",
"ember-route-action-helper": "2.0.6",
"ember-simple-auth": "1.6.0",
"ember-sinon": "2.1.0",
"ember-source": "3.1.2",
"ember-sticky-element": "0.2.2",
"ember-svg-jar": "1.2.0",
"ember-source": "3.5.1",
"ember-sticky-element": "0.2.3",
"ember-svg-jar": "1.2.2",
"ember-test-selectors": "1.0.0",
"ember-truth-helpers": "2.1.0",
"ember-useragent": "0.6.1",
@ -118,7 +119,7 @@
"password-generator": "2.2.0",
"reframe.js": "2.2.1",
"simplemde": "https://github.com/kevinansfield/simplemde-markdown-editor.git#ghost",
"testem": "2.8.2",
"testem": "2.14.0",
"top-gh-contribs": "2.0.4",
"validator": "7.2.0",
"walk-sync": "0.3.2"

View file

@ -14,13 +14,14 @@ module.exports = {
],
browser_args: {
Chrome: {
mode: 'ci',
args: [
ci: [
// --no-sandbox is needed when running Chrome inside a container
process.env.TRAVIS ? '--no-sandbox' : null,
'--disable-gpu',
process.env.CI ? '--no-sandbox' : null,
'--headless',
'--disable-gpu',
'--disable-dev-shm-usage',
'--disable-software-rasterizer',
'--mute-audio',
'--remote-debugging-port=0',
'--window-size=1440,900'
].filter(Boolean)

View file

@ -3,19 +3,5 @@ module.exports = {
env: {
'embertest': true,
'mocha': true
},
globals: {
server: false,
expect: false,
fileUpload: false,
// ember-power-select test helpers
selectChoose: false,
selectSearch: false,
removeMultipleOption: false,
clearSelected: false,
// ember-power-datepicker test helpers
datepickerSelect: false
}
};

View file

@ -1,29 +1,24 @@
import destroyApp from '../helpers/destroy-app';
import startApp from '../helpers/start-app';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import windowProxy from 'ghost-admin/utils/window-proxy';
import {Response} from 'ember-cli-mirage';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {click, currentRouteName, currentURL, fillIn, findAll, visit} from '@ember/test-helpers';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupApplicationTest} from 'ember-mocha';
describe('Acceptance: Authentication', function () {
let application,
originalReplaceLocation;
let originalReplaceLocation;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
describe('setup redirect', function () {
beforeEach(function () {
// ensure the /users/me route doesn't error
server.create('user');
server.get('authentication/setup', function () {
this.server.create('user');
this.server.get('authentication/setup', function () {
return {setup: [{status: false}]};
});
});
@ -44,8 +39,8 @@ describe('Acceptance: Authentication', function () {
};
newLocation = undefined;
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role], slug: 'test-user'});
});
afterEach(function () {
@ -54,13 +49,13 @@ describe('Acceptance: Authentication', function () {
it('invalidates session on 401 API response', async function () {
// return a 401 when attempting to retrieve users
server.get('/users/', () => new Response(401, {}, {
this.server.get('/users/', () => new Response(401, {}, {
errors: [
{message: 'Access denied.', errorType: 'UnauthorizedError'}
]
}));
await authenticateSession(application);
await authenticateSession();
await visit('/team');
// running `visit(url)` inside windowProxy.replaceLocation breaks
@ -74,27 +69,27 @@ describe('Acceptance: Authentication', function () {
});
it('doesn\'t show navigation menu on invalid url when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/');
expect(currentURL(), 'current url').to.equal('/signin');
expect(find('nav.gh-nav').length, 'nav menu presence').to.equal(0);
expect(findAll('nav.gh-nav').length, 'nav menu presence').to.equal(0);
await visit('/signin/invalidurl/');
expect(currentURL(), 'url after invalid url').to.equal('/signin/invalidurl/');
expect(currentPath(), 'path after invalid url').to.equal('error404');
expect(find('nav.gh-nav').length, 'nav menu presence').to.equal(0);
expect(currentRouteName(), 'path after invalid url').to.equal('error404');
expect(findAll('nav.gh-nav').length, 'nav menu presence').to.equal(0);
});
it('shows nav menu on invalid url when authenticated', async function () {
await authenticateSession(application);
await authenticateSession();
await visit('/signin/invalidurl/');
expect(currentURL(), 'url after invalid url').to.equal('/signin/invalidurl/');
expect(currentPath(), 'path after invalid url').to.equal('error404');
expect(find('nav.gh-nav').length, 'nav menu presence').to.equal(1);
expect(currentRouteName(), 'path after invalid url').to.equal('error404');
expect(findAll('nav.gh-nav').length, 'nav menu presence').to.equal(1);
});
});
@ -110,11 +105,11 @@ describe('Acceptance: Authentication', function () {
});
it('displays re-auth modal attempting to save with invalid session', async function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
// simulate an invalid session when saving the edited post
server.put('/posts/:id/', function ({posts}, {params}) {
this.server.put('/posts/:id/', function ({posts}, {params}) {
let post = posts.find(params.id);
let attrs = this.normalizedRequestAttrs();
@ -129,7 +124,7 @@ describe('Acceptance: Authentication', function () {
}
});
await authenticateSession(application);
await authenticateSession();
await visit('/editor');
@ -139,16 +134,16 @@ describe('Acceptance: Authentication', function () {
await click('.js-publish-button');
// we shouldn't have a modal at this point
expect(find('.modal-container #login').length, 'modal exists').to.equal(0);
expect(findAll('.modal-container #login').length, 'modal exists').to.equal(0);
// we also shouldn't have any alerts
expect(find('.gh-alert').length, 'no of alerts').to.equal(0);
expect(findAll('.gh-alert').length, 'no of alerts').to.equal(0);
// update the post
await fillIn('.__mobiledoc-editor', 'Edited post body');
await click('.js-publish-button');
// we should see a re-auth modal
expect(find('.fullscreen-modal #login').length, 'modal exists').to.equal(1);
expect(findAll('.fullscreen-modal #login').length, 'modal exists').to.equal(1);
});
// don't clobber debounce/throttle for future tests

View file

@ -1,26 +1,17 @@
import destroyApp from '../helpers/destroy-app';
import startApp from '../helpers/start-app';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {
authenticateSession,
invalidateSession
} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {clickTrigger} from 'ember-power-select/test-support/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {clickTrigger, selectChoose} from 'ember-power-select/test-support/helpers';
import {currentURL, find, findAll, triggerEvent, visit} from '@ember/test-helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
describe('Acceptance: Content', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/');
expect(currentURL()).to.equal('/signin');
@ -30,47 +21,47 @@ describe('Acceptance: Content', function () {
let admin, editor,
publishedPost, scheduledPost, draftPost, publishedPage, authorPost;
beforeEach(function () {
let adminRole = server.create('role', {name: 'Administrator'});
admin = server.create('user', {roles: [adminRole]});
let editorRole = server.create('role', {name: 'Editor'});
editor = server.create('user', {roles: [editorRole]});
beforeEach(async function () {
let adminRole = this.server.create('role', {name: 'Administrator'});
admin = this.server.create('user', {roles: [adminRole]});
let editorRole = this.server.create('role', {name: 'Editor'});
editor = this.server.create('user', {roles: [editorRole]});
publishedPost = server.create('post', {authors: [admin], status: 'published', title: 'Published Post'});
scheduledPost = server.create('post', {authors: [admin], status: 'scheduled', title: 'Scheduled Post'});
draftPost = server.create('post', {authors: [admin], status: 'draft', title: 'Draft Post'});
publishedPage = server.create('post', {authors: [admin], status: 'published', page: true, title: 'Published Page'});
authorPost = server.create('post', {authors: [editor], status: 'published', title: 'Editor Published Post'});
publishedPost = this.server.create('post', {authors: [admin], status: 'published', title: 'Published Post'});
scheduledPost = this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Scheduled Post'});
draftPost = this.server.create('post', {authors: [admin], status: 'draft', title: 'Draft Post'});
publishedPage = this.server.create('post', {authors: [admin], status: 'published', page: true, title: 'Published Page'});
authorPost = this.server.create('post', {authors: [editor], status: 'published', title: 'Editor Published Post'});
return authenticateSession(application);
return await authenticateSession();
});
it('displays and filters posts', async function () {
await visit('/');
// Not checking request here as it won't be the last request made
// Displays all posts + pages
expect(find('[data-test-post-id]').length, 'all posts count').to.equal(5);
expect(findAll('[data-test-post-id]').length, 'all posts count').to.equal(5);
// show draft posts
await selectChoose('[data-test-type-select]', 'Draft posts');
// API request is correct
let [lastRequest] = server.pretender.handledRequests.slice(-1);
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"drafts" request status filter').to.have.string('status:draft');
expect(lastRequest.queryParams.filter, '"drafts" request page filter').to.have.string('page:false');
// Displays draft post
expect(find('[data-test-post-id]').length, 'drafts count').to.equal(1);
expect(findAll('[data-test-post-id]').length, 'drafts count').to.equal(1);
expect(find(`[data-test-post-id="${draftPost.id}"]`), 'draft post').to.exist;
// show published posts
await selectChoose('[data-test-type-select]', 'Published posts');
// API request is correct
[lastRequest] = server.pretender.handledRequests.slice(-1);
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"published" request status filter').to.have.string('status:published');
expect(lastRequest.queryParams.filter, '"published" request page filter').to.have.string('page:false');
// Displays three published posts + pages
expect(find('[data-test-post-id]').length, 'published count').to.equal(2);
expect(findAll('[data-test-post-id]').length, 'published count').to.equal(2);
expect(find(`[data-test-post-id="${publishedPost.id}"]`), 'admin published post').to.exist;
expect(find(`[data-test-post-id="${authorPost.id}"]`), 'author published post').to.exist;
@ -78,29 +69,29 @@ describe('Acceptance: Content', function () {
await selectChoose('[data-test-type-select]', 'Scheduled posts');
// API request is correct
[lastRequest] = server.pretender.handledRequests.slice(-1);
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"scheduled" request status filter').to.have.string('status:scheduled');
expect(lastRequest.queryParams.filter, '"scheduled" request page filter').to.have.string('page:false');
// Displays scheduled post
expect(find('[data-test-post-id]').length, 'scheduled count').to.equal(1);
expect(findAll('[data-test-post-id]').length, 'scheduled count').to.equal(1);
expect(find(`[data-test-post-id="${scheduledPost.id}"]`), 'scheduled post').to.exist;
// show pages
await selectChoose('[data-test-type-select]', 'Pages');
// API request is correct
[lastRequest] = server.pretender.handledRequests.slice(-1);
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"pages" request status filter').to.have.string('status:[draft,scheduled,published]');
expect(lastRequest.queryParams.filter, '"pages" request page filter').to.have.string('page:true');
// Displays page
expect(find('[data-test-post-id]').length, 'pages count').to.equal(1);
expect(findAll('[data-test-post-id]').length, 'pages count').to.equal(1);
expect(find(`[data-test-post-id="${publishedPage.id}"]`), 'page post').to.exist;
// show all posts
await selectChoose('[data-test-type-select]', 'All posts');
// API request is correct
[lastRequest] = server.pretender.handledRequests.slice(-1);
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"all" request status filter').to.have.string('status:[draft,scheduled,published]');
expect(lastRequest.queryParams.filter, '"all" request page filter').to.have.string('page:[true,false]');
@ -108,7 +99,7 @@ describe('Acceptance: Content', function () {
await selectChoose('[data-test-author-select]', editor.name);
// API request is correct
[lastRequest] = server.pretender.handledRequests.slice(-1);
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"editor" request status filter').to.have.string('status:[draft,scheduled,published]');
expect(lastRequest.queryParams.filter, '"editor" request page filter').to.have.string('page:[true,false]');
expect(lastRequest.queryParams.filter, '"editor" request filter param').to.have.string(`authors:${editor.slug}`);
@ -127,14 +118,14 @@ describe('Acceptance: Content', function () {
});
it('sorts tags filter alphabetically', async function () {
server.create('tag', {name: 'B - Second', slug: 'second'});
server.create('tag', {name: 'Z - Last', slug: 'last'});
server.create('tag', {name: 'A - First', slug: 'first'});
this.server.create('tag', {name: 'B - Second', slug: 'second'});
this.server.create('tag', {name: 'Z - Last', slug: 'last'});
this.server.create('tag', {name: 'A - First', slug: 'first'});
await visit('/');
await clickTrigger('[data-test-tag-select]');
let options = find('.ember-power-select-option');
let options = findAll('.ember-power-select-option');
expect(options[0].textContent.trim()).to.equal('All tags');
expect(options[1].textContent.trim()).to.equal('A - First');
@ -146,17 +137,17 @@ describe('Acceptance: Content', function () {
describe('as author', function () {
let author, authorPost;
beforeEach(function () {
let authorRole = server.create('role', {name: 'Author'});
author = server.create('user', {roles: [authorRole]});
let adminRole = server.create('role', {name: 'Administrator'});
let admin = server.create('user', {roles: [adminRole]});
beforeEach(async function () {
let authorRole = this.server.create('role', {name: 'Author'});
author = this.server.create('user', {roles: [authorRole]});
let adminRole = this.server.create('role', {name: 'Administrator'});
let admin = this.server.create('user', {roles: [adminRole]});
// create posts
authorPost = server.create('post', {authors: [author], status: 'published', title: 'Author Post'});
server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
authorPost = this.server.create('post', {authors: [author], status: 'published', title: 'Author Post'});
this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
return authenticateSession(application);
return await authenticateSession();
});
it('only fetches the author\'s posts', async function () {
@ -165,11 +156,11 @@ describe('Acceptance: Content', function () {
await selectChoose('[data-test-type-select]', 'Published posts');
// API request includes author filter
let [lastRequest] = server.pretender.handledRequests.slice(-1);
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter).to.have.string(`authors:${author.slug}`);
// only author's post is shown
expect(find('[data-test-post-id]').length, 'post count').to.equal(1);
expect(findAll('[data-test-post-id]').length, 'post count').to.equal(1);
expect(find(`[data-test-post-id="${authorPost.id}"]`), 'author post').to.exist;
});
});
@ -177,18 +168,18 @@ describe('Acceptance: Content', function () {
describe('as contributor', function () {
let contributor, contributorPost;
beforeEach(function () {
let contributorRole = server.create('role', {name: 'Contributor'});
contributor = server.create('user', {roles: [contributorRole]});
let adminRole = server.create('role', {name: 'Administrator'});
let admin = server.create('user', {roles: [adminRole]});
beforeEach(async function () {
let contributorRole = this.server.create('role', {name: 'Contributor'});
contributor = this.server.create('user', {roles: [contributorRole]});
let adminRole = this.server.create('role', {name: 'Administrator'});
let admin = this.server.create('user', {roles: [adminRole]});
// Create posts
contributorPost = server.create('post', {authors: [contributor], status: 'draft', title: 'Contributor Post Draft'});
server.create('post', {authors: [contributor], status: 'published', title: 'Contributor Published Post'});
server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
contributorPost = this.server.create('post', {authors: [contributor], status: 'draft', title: 'Contributor Post Draft'});
this.server.create('post', {authors: [contributor], status: 'published', title: 'Contributor Published Post'});
this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
return authenticateSession(application);
return await authenticateSession();
});
it('only fetches the contributor\'s draft posts', async function () {
@ -203,11 +194,11 @@ describe('Acceptance: Content', function () {
await selectChoose('[data-test-order-select]', 'Oldest');
// API request includes author filter
let [lastRequest] = server.pretender.handledRequests.slice(-1);
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter).to.have.string(`authors:${contributor.slug}`);
// only contributor's post is shown
expect(find('[data-test-post-id]').length, 'post count').to.equal(1);
expect(findAll('[data-test-post-id]').length, 'post count').to.equal(1);
expect(find(`[data-test-post-id="${contributorPost.id}"]`), 'author post').to.exist;
});
});

View file

@ -1,35 +1,30 @@
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import destroyApp from 'ghost-admin/tests/helpers/destroy-app';
import startApp from 'ghost-admin/tests/helpers/start-app';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {click, fillIn, find, keyEvent, visit} from 'ember-native-dom-helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {click, fillIn, find, triggerKeyEvent, visit} from '@ember/test-helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
// keyCodes
const KEY_S = 83;
describe('Acceptance: Custom Post Templates', function () {
let application;
let hooks = setupApplicationTest();
setupMirage(hooks);
beforeEach(function () {
application = startApp();
beforeEach(async function () {
this.server.loadFixtures('settings');
server.loadFixtures('settings');
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
authenticateSession(application);
});
afterEach(function () {
destroyApp(application);
return await authenticateSession();
});
describe('with custom templates', function () {
beforeEach(function () {
server.create('theme', {
this.server.create('theme', {
active: true,
name: 'example-theme',
package: {
@ -66,7 +61,7 @@ describe('Acceptance: Custom Post Templates', function () {
});
it('can change selected template', async function () {
let post = server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});
let post = this.server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});
await visit('/editor/1');
await click('[data-test-psm-trigger]');
@ -91,13 +86,13 @@ describe('Acceptance: Custom Post Templates', function () {
await fillIn(select, '');
// save then check server record
await keyEvent('.gh-app', 'keydown', KEY_S, {
await triggerKeyEvent('.gh-app', 'keydown', KEY_S, {
metaKey: ctrlOrCmd === 'command',
ctrlKey: ctrlOrCmd === 'ctrl'
});
expect(
server.db.posts.find(post.id).customTemplate,
this.server.db.posts.find(post.id).customTemplate,
'saved custom template'
).to.equal('');
});
@ -105,13 +100,14 @@ describe('Acceptance: Custom Post Templates', function () {
it('disables template selector if slug matches slug-based template');
it('doesn\'t query themes endpoint unncessarily', async function () {
function themeRequests() {
return server.pretender.handledRequests.filter(function (request) {
// eslint-disable-next-line
let themeRequests = () => {
return this.server.pretender.handledRequests.filter(function (request) {
return request.url.match(/\/themes\//);
});
}
};
server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});
this.server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});
await visit('/editor/1');
await click('[data-test-psm-trigger]');
@ -127,7 +123,7 @@ describe('Acceptance: Custom Post Templates', function () {
describe('without custom templates', function () {
beforeEach(function () {
server.create('theme', {
this.server.create('theme', {
active: true,
name: 'example-theme',
package: {
@ -139,7 +135,7 @@ describe('Acceptance: Custom Post Templates', function () {
});
it('doesn\'t show template selector', async function () {
server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});
this.server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});
await visit('/editor/1');
await click('[data-test-psm-trigger]');

View file

@ -1,84 +1,83 @@
import Mirage from 'ember-cli-mirage';
import destroyApp from '../helpers/destroy-app';
import moment from 'moment';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import sinon from 'sinon';
import startApp from '../helpers/start-app';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {blur, click, currentRouteName, currentURL, fillIn, find, findAll, triggerEvent} from '@ember/test-helpers';
import {datepickerSelect} from 'ember-power-datepicker/test-support';
import {expect} from 'chai';
// import {selectChoose} from 'ember-power-select/test-support';
import {selectChoose} from 'ember-power-select/test-support';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../helpers/visit';
// TODO: update ember-power-datepicker to expose modern test helpers
// https://github.com/cibernox/ember-power-datepicker/issues/30
describe('Acceptance: Editor', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
let author = server.create('user'); // necesary for post-author association
server.create('post', {authors: [author]});
let author = this.server.create('user'); // necesary for post-author association
this.server.create('post', {authors: [author]});
invalidateSession(application);
await invalidateSession();
await visit('/editor/1');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('does not redirect to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
let author = server.create('user', {roles: [role], slug: 'test-user'});
server.create('post', {authors: [author]});
let role = this.server.create('role', {name: 'Contributor'});
let author = this.server.create('user', {roles: [role], slug: 'test-user'});
this.server.create('post', {authors: [author]});
authenticateSession(application);
await authenticateSession();
await visit('/editor/1');
expect(currentURL(), 'currentURL').to.equal('/editor/1');
});
it('does not redirect to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
let author = server.create('user', {roles: [role], slug: 'test-user'});
server.create('post', {authors: [author]});
let role = this.server.create('role', {name: 'Author'});
let author = this.server.create('user', {roles: [role], slug: 'test-user'});
this.server.create('post', {authors: [author]});
authenticateSession(application);
await authenticateSession();
await visit('/editor/1');
expect(currentURL(), 'currentURL').to.equal('/editor/1');
});
it('does not redirect to team page when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
let author = server.create('user', {roles: [role], slug: 'test-user'});
server.create('post', {authors: [author]});
let role = this.server.create('role', {name: 'Editor'});
let author = this.server.create('user', {roles: [role], slug: 'test-user'});
this.server.create('post', {authors: [author]});
authenticateSession(application);
await authenticateSession();
await visit('/editor/1');
expect(currentURL(), 'currentURL').to.equal('/editor/1');
});
it('displays 404 when post does not exist', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/editor/1');
expect(currentPath()).to.equal('error404');
expect(currentRouteName()).to.equal('error404');
expect(currentURL()).to.equal('/editor/1');
});
it('when logged in as a contributor, renders a save button instead of a publish menu & hides tags input', async function () {
let role = server.create('role', {name: 'Contributor'});
let author = server.create('user', {roles: [role]});
server.createList('post', 2, {authors: [author]});
server.loadFixtures('settings');
authenticateSession(application);
let role = this.server.create('role', {name: 'Contributor'});
let author = this.server.create('user', {roles: [role]});
this.server.createList('post', 2, {authors: [author]});
this.server.loadFixtures('settings');
await authenticateSession();
// post id 1 is a draft, checking for draft behaviour now
await visit('/editor/1');
@ -109,16 +108,16 @@ describe('Acceptance: Editor', function () {
describe('when logged in', function () {
let author;
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
author = server.create('user', {roles: [role]});
server.loadFixtures('settings');
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
author = this.server.create('user', {roles: [role]});
this.server.loadFixtures('settings');
return authenticateSession(application);
return await authenticateSession();
});
it('renders the editor correctly, PSM Publish Date and Save Button', async function () {
let [post1] = server.createList('post', 2, {authors: [author]});
let [post1] = this.server.createList('post', 2, {authors: [author]});
let futureTime = moment().tz('Etc/UTC').add(10, 'minutes');
// post id 1 is a draft, checking for draft behaviour now
@ -132,19 +131,19 @@ describe('Acceptance: Editor', function () {
// should error, if the publish time is in the wrong format
await fillIn('[data-test-date-time-picker-time-input]', 'foo');
await triggerEvent('[data-test-date-time-picker-time-input]', 'blur');
await blur('[data-test-date-time-picker-time-input]');
expect(find('[data-test-date-time-picker-error]').text().trim(), 'inline error response for invalid time')
expect(find('[data-test-date-time-picker-error]').textContent.trim(), 'inline error response for invalid time')
.to.equal('Must be in format: "15:00"');
// should error, if the publish time is in the future
// NOTE: date must be selected first, changing the time first will save
// with the new time
await datepickerSelect('[data-test-date-time-picker-datepicker]', moment.tz('Etc/UTC'));
await datepickerSelect('[data-test-date-time-picker-datepicker]', moment.tz('Etc/UTC').toDate());
await fillIn('[data-test-date-time-picker-time-input]', futureTime.format('HH:mm'));
await triggerEvent('[data-test-date-time-picker-time-input]', 'blur');
await blur('[data-test-date-time-picker-time-input]');
expect(find('[data-test-date-time-picker-error]').text().trim(), 'inline error response for future time')
expect(find('[data-test-date-time-picker-error]').textContent.trim(), 'inline error response for future time')
.to.equal('Must be in the past');
// closing the PSM will reset the invalid date/time
@ -152,37 +151,37 @@ describe('Acceptance: Editor', function () {
await click('[data-test-psm-trigger]');
expect(
find('[data-test-date-time-picker-error]').text().trim(),
find('[data-test-date-time-picker-error]'),
'date picker error after closing PSM'
).to.equal('');
).to.not.exist;
expect(
find('[data-test-date-time-picker-date-input]').val(),
find('[data-test-date-time-picker-date-input]').value,
'PSM date value after closing with invalid date'
).to.equal(moment(post1.publishedAt).tz('Etc/UTC').format('MM/DD/YYYY'));
expect(
find('[data-test-date-time-picker-time-input]').val(),
find('[data-test-date-time-picker-time-input]').value,
'PSM time value after closing with invalid date'
).to.equal(moment(post1.publishedAt).tz('Etc/UTC').format('HH:mm'));
// saves the post with the new date
let validTime = moment('2017-04-09 12:00').tz('Etc/UTC');
await fillIn('[data-test-date-time-picker-time-input]', validTime.format('HH:mm'));
await triggerEvent('[data-test-date-time-picker-time-input]', 'blur');
await datepickerSelect('[data-test-date-time-picker-datepicker]', validTime);
await blur('[data-test-date-time-picker-time-input]');
await datepickerSelect('[data-test-date-time-picker-datepicker]', validTime.toDate());
// hide psm
await click('[data-test-close-settings-menu]');
// checking the flow of the saving button for a draft
expect(
find('[data-test-publishmenu-trigger]').text().trim(),
find('[data-test-publishmenu-trigger]').textContent.trim(),
'draft publish button text'
).to.equal('Publish');
expect(
find('[data-test-editor-post-status]').text().trim(),
find('[data-test-editor-post-status]').textContent.trim(),
'draft status text'
).to.equal('Draft');
@ -197,14 +196,14 @@ describe('Acceptance: Editor', function () {
await click('[data-test-publishmenu-scheduled-option]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'draft post schedule button text'
).to.equal('Schedule');
await click('[data-test-publishmenu-published-option]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'draft post publish button text'
).to.equal('Publish');
@ -212,7 +211,7 @@ describe('Acceptance: Editor', function () {
await click('[data-test-publishmenu-save]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'publish menu save button updated after draft is published'
).to.equal('Published');
@ -222,7 +221,7 @@ describe('Acceptance: Editor', function () {
).to.exist;
expect(
find('[data-test-editor-post-status]').text().trim(),
find('[data-test-editor-post-status]').textContent.trim(),
'post status updated after draft published'
).to.equal('Published');
@ -231,7 +230,7 @@ describe('Acceptance: Editor', function () {
await click('[data-test-publishmenu-unpublished-option]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'published post unpublish button text'
).to.equal('Unpublish');
@ -239,25 +238,25 @@ describe('Acceptance: Editor', function () {
await visit('/editor/2');
expect(currentURL(), 'currentURL').to.equal('/editor/2');
expect(find('[data-test-date-time-picker-date-input]').val()).to.equal('12/19/2015');
expect(find('[data-test-date-time-picker-time-input]').val()).to.equal('16:25');
expect(find('[data-test-date-time-picker-date-input]').value).to.equal('12/19/2015');
expect(find('[data-test-date-time-picker-time-input]').value).to.equal('16:25');
// saves the post with a new date
await datepickerSelect('[data-test-date-time-picker-datepicker]', moment('2016-05-10 10:00'));
await datepickerSelect('[data-test-date-time-picker-datepicker]', moment('2016-05-10 10:00').toDate());
await fillIn('[data-test-date-time-picker-time-input]', '10:00');
await triggerEvent('[data-test-date-time-picker-time-input]', 'blur');
await blur('[data-test-date-time-picker-time-input]');
// saving
await click('[data-test-publishmenu-trigger]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'published button text'
).to.equal('Update');
await click('[data-test-publishmenu-save]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'publish menu save button updated after published post is updated'
).to.equal('Updated');
@ -267,17 +266,17 @@ describe('Acceptance: Editor', function () {
expect(currentURL(), 'currentURL for settings')
.to.equal('/settings/general');
expect(find('#activeTimezone option:selected').text().trim(), 'default timezone')
expect(find('#activeTimezone option:checked').textContent.trim(), 'default timezone')
.to.equal('(GMT) UTC');
// select a new timezone
find('#activeTimezone option[value="Pacific/Kwajalein"]').prop('selected', true);
find('#activeTimezone option[value="Pacific/Kwajalein"]').selected = true;
await triggerEvent('#activeTimezone', 'change');
// save the settings
await click('.gh-btn.gh-btn-blue');
expect(find('#activeTimezone option:selected').text().trim(), 'new timezone after saving')
expect(find('#activeTimezone option:checked').textContent.trim(), 'new timezone after saving')
.to.equal('(GMT +12:00) International Date Line West');
// and now go back to the editor
@ -287,12 +286,12 @@ describe('Acceptance: Editor', function () {
.to.equal('/editor/2');
expect(
find('[data-test-date-time-picker-date-input]').val(),
find('[data-test-date-time-picker-date-input]').value,
'date after timezone change'
).to.equal('05/10/2016');
expect(
find('[data-test-date-time-picker-time-input]').val(),
find('[data-test-date-time-picker-time-input]').value,
'time after timezone change'
).to.equal('22:00');
@ -301,14 +300,14 @@ describe('Acceptance: Editor', function () {
await click('[data-test-publishmenu-unpublished-option]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'published post unpublish button text'
).to.equal('Unpublish');
await click('[data-test-publishmenu-save]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'publish menu save button updated after published post is unpublished'
).to.equal('Unpublished');
@ -318,7 +317,7 @@ describe('Acceptance: Editor', function () {
).to.exist;
expect(
find('[data-test-editor-post-status]').text().trim(),
find('[data-test-editor-post-status]').textContent.trim(),
'post status updated after unpublished'
).to.equal('Draft');
@ -330,15 +329,15 @@ describe('Acceptance: Editor', function () {
await click('[data-test-publishmenu-scheduled-option]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'draft post, schedule button text'
).to.equal('Schedule');
await datepickerSelect('[data-test-publishmenu-draft] [data-test-date-time-picker-datepicker]', newFutureTime);
await datepickerSelect('[data-test-publishmenu-draft] [data-test-date-time-picker-datepicker]', new Date(newFutureTime.format().replace(/\+.*$/, '')));
await click('[data-test-publishmenu-save]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'publish menu save button updated after draft is scheduled'
).to.equal('Scheduled');
@ -350,16 +349,16 @@ describe('Acceptance: Editor', function () {
).to.not.exist;
// expect countdown to show warning, that post will go live in x minutes
expect(find('[data-test-schedule-countdown]').text().trim(), 'notification countdown')
expect(find('[data-test-schedule-countdown]').textContent.trim(), 'notification countdown')
.to.contain('Post will go live in');
expect(
find('[data-test-publishmenu-trigger]').text().trim(),
find('[data-test-publishmenu-trigger]').textContent.trim(),
'scheduled publish button text'
).to.equal('Scheduled');
expect(
find('[data-test-editor-post-status]').text().trim(),
find('[data-test-editor-post-status]').textContent.trim(),
'scheduled post status'
).to.equal('Scheduled');
@ -367,14 +366,14 @@ describe('Acceptance: Editor', function () {
await click('[data-test-publishmenu-trigger]');
await click('[data-test-publishmenu-scheduled-option]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'scheduled post button reschedule text'
).to.equal('Reschedule');
await click('[data-test-publishmenu-save]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'publish menu save button text for a rescheduled post'
).to.equal('Rescheduled');
@ -386,7 +385,7 @@ describe('Acceptance: Editor', function () {
).to.not.exist;
expect(
find('[data-test-editor-post-status]').text().trim(),
find('[data-test-editor-post-status]').textContent.trim(),
'scheduled status text'
).to.equal('Scheduled');
@ -395,26 +394,26 @@ describe('Acceptance: Editor', function () {
await click('[data-test-publishmenu-draft-option]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'publish menu save button updated after scheduled post is unscheduled'
).to.equal('Unschedule');
await click('[data-test-publishmenu-save]');
expect(
find('[data-test-publishmenu-save]').text().trim(),
find('[data-test-publishmenu-save]').textContent.trim(),
'publish menu save button updated after scheduled post is unscheduled'
).to.equal('Unscheduled');
await click('[data-test-publishmenu-cancel]');
expect(
find('[data-test-publishmenu-trigger]').text().trim(),
find('[data-test-publishmenu-trigger]').textContent.trim(),
'publish button text after unschedule'
).to.equal('Publish');
expect(
find('[data-test-editor-post-status]').text().trim(),
find('[data-test-editor-post-status]').textContent.trim(),
'status text after unschedule'
).to.equal('Draft');
@ -425,7 +424,7 @@ describe('Acceptance: Editor', function () {
});
it('handles validation errors when scheduling', async function () {
server.put('/posts/:id/', function () {
this.server.put('/posts/:id/', function () {
return new Mirage.Response(422, {}, {
errors: [{
errorType: 'ValidationError',
@ -434,32 +433,32 @@ describe('Acceptance: Editor', function () {
});
});
let post = server.create('post', 1, {authors: [author], status: 'draft'});
let post = this.server.create('post', 1, {authors: [author], status: 'draft'});
let plusTenMin = moment().utc().add(10, 'minutes');
await visit(`/editor/${post.id}`);
await click('[data-test-publishmenu-trigger]');
await click('[data-test-publishmenu-scheduled-option]');
await datepickerSelect('[data-test-publishmenu-draft] [data-test-date-time-picker-datepicker]', plusTenMin);
await datepickerSelect('[data-test-publishmenu-draft] [data-test-date-time-picker-datepicker]', plusTenMin.toDate());
await fillIn('[data-test-publishmenu-draft] [data-test-date-time-picker-time-input]', plusTenMin.format('HH:mm'));
await triggerEvent('[data-test-publishmenu-draft] [data-test-date-time-picker-time-input]', 'blur');
await blur('[data-test-publishmenu-draft] [data-test-date-time-picker-time-input]');
await click('[data-test-publishmenu-save]');
expect(
find('.gh-alert').length,
findAll('.gh-alert').length,
'number of alerts after failed schedule'
).to.equal(1);
expect(
find('.gh-alert').text(),
find('.gh-alert').textContent,
'alert text after failed schedule'
).to.match(/Saving failed: Error test/);
});
it('handles title validation errors correctly', async function () {
server.create('post', {authors: [author]});
this.server.create('post', {authors: [author]});
// post id 1 is a draft, checking for draft behaviour now
await visit('/editor/1');
@ -472,19 +471,19 @@ describe('Acceptance: Editor', function () {
await click('[data-test-publishmenu-save]');
expect(
find('.gh-alert').length,
findAll('.gh-alert').length,
'number of alerts after invalid title'
).to.equal(1);
expect(
find('.gh-alert').text(),
find('.gh-alert').textContent,
'alert text after invalid title'
).to.match(/Title cannot be longer than 255 characters/);
});
// NOTE: these tests are specific to the mobiledoc editor
// it('inserts a placeholder if the title is blank', async function () {
// server.createList('post', 1);
// this.server.createList('post', 1);
//
// // post id 1 is a draft, checking for draft behaviour now
// await visit('/editor/1');
@ -506,7 +505,7 @@ describe('Acceptance: Editor', function () {
// });
//
// it('removes HTML from the title.', async function () {
// server.createList('post', 1);
// this.server.createList('post', 1);
//
// // post id 1 is a draft, checking for draft behaviour now
// await visit('/editor/1');
@ -526,32 +525,32 @@ describe('Acceptance: Editor', function () {
let compareDate = moment().tz('Etc/UTC').add(4, 'minutes');
let compareDateString = compareDate.format('MM/DD/YYYY');
let compareTimeString = compareDate.format('HH:mm');
server.create('post', {publishedAt: moment.utc().add(4, 'minutes'), status: 'scheduled', authors: [author]});
server.create('setting', {activeTimezone: 'Europe/Dublin'});
this.server.create('post', {publishedAt: moment.utc().add(4, 'minutes'), status: 'scheduled', authors: [author]});
this.server.create('setting', {activeTimezone: 'Europe/Dublin'});
clock.restore();
await visit('/editor/1');
expect(currentURL(), 'currentURL')
.to.equal('/editor/1');
expect(find('[data-test-date-time-picker-date-input]').val(), 'scheduled date')
expect(find('[data-test-date-time-picker-date-input]').value, 'scheduled date')
.to.equal(compareDateString);
expect(find('[data-test-date-time-picker-time-input]').val(), 'scheduled time')
expect(find('[data-test-date-time-picker-time-input]').value, 'scheduled time')
.to.equal(compareTimeString);
// Dropdown menu should be 'Update Post' and 'Unschedule'
expect(find('[data-test-publishmenu-trigger]').text().trim(), 'text in save button for scheduled post')
expect(find('[data-test-publishmenu-trigger]').textContent.trim(), 'text in save button for scheduled post')
.to.equal('Scheduled');
// expect countdown to show warning, that post will go live in x minutes
expect(find('[data-test-schedule-countdown]').text().trim(), 'notification countdown')
expect(find('[data-test-schedule-countdown]').textContent.trim(), 'notification countdown')
.to.contain('Post will go live in');
});
it('shows author token input and allows changing of authors in PSM', async function () {
let adminRole = server.create('role', {name: 'Adminstrator'});
let authorRole = server.create('role', {name: 'Author'});
let user1 = server.create('user', {name: 'Primary', roles: [adminRole]});
server.create('user', {name: 'Waldo', roles: [authorRole]});
server.create('post', {authors: [user1]});
let adminRole = this.server.create('role', {name: 'Adminstrator'});
let authorRole = this.server.create('role', {name: 'Author'});
let user1 = this.server.create('user', {name: 'Primary', roles: [adminRole]});
this.server.create('user', {name: 'Waldo', roles: [authorRole]});
this.server.create('post', {authors: [user1]});
await visit('/editor/1');
@ -560,14 +559,14 @@ describe('Acceptance: Editor', function () {
await click('button.post-settings');
let tokens = find('[data-test-input="authors"] .ember-power-select-multiple-option');
let tokens = findAll('[data-test-input="authors"] .ember-power-select-multiple-option');
expect(tokens.length).to.equal(1);
expect(tokens[0].textContent.trim()).to.have.string('Primary');
await selectChoose('[data-test-input="authors"]', 'Waldo');
let savedAuthors = server.schema.posts.find('1').authors.models;
let savedAuthors = this.server.schema.posts.find('1').authors.models;
expect(savedAuthors.length).to.equal(2);
expect(savedAuthors[0].name).to.equal('Primary');
@ -575,8 +574,8 @@ describe('Acceptance: Editor', function () {
});
it('autosaves when title loses focus', async function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {name: 'Admin', roles: [role]});
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {name: 'Admin', roles: [role]});
await visit('/editor');
@ -589,10 +588,11 @@ describe('Acceptance: Editor', function () {
'url on initial visit'
).to.equal('/editor');
await triggerEvent('[data-test-editor-title-input]', 'blur');
await click('[data-test-editor-title-input]');
await blur('[data-test-editor-title-input]');
expect(
find('[data-test-editor-title-input]').val(),
find('[data-test-editor-title-input]').value,
'title value after autosave'
).to.equal('(Untitled)');
@ -603,7 +603,7 @@ describe('Acceptance: Editor', function () {
});
it('saves post settings fields', async function () {
let post = server.create('post', {authors: [author]});
let post = this.server.create('post', {authors: [author]});
await visit(`/editor/${post.id}`);
@ -613,24 +613,24 @@ describe('Acceptance: Editor', function () {
// excerpt has validation
await fillIn('[data-test-field="custom-excerpt"]', Array(302).join('a'));
await triggerEvent('[data-test-field="custom-excerpt"]', 'blur');
await blur('[data-test-field="custom-excerpt"]');
expect(
find('[data-test-error="custom-excerpt"]').text().trim(),
find('[data-test-error="custom-excerpt"]').textContent.trim(),
'excerpt too long error'
).to.match(/cannot be longer than 300/);
expect(
server.db.posts.find(post.id).customExcerpt,
this.server.db.posts.find(post.id).customExcerpt,
'saved excerpt after validation error'
).to.be.null;
// changing custom excerpt auto-saves
await fillIn('[data-test-field="custom-excerpt"]', 'Testing excerpt');
await triggerEvent('[data-test-field="custom-excerpt"]', 'blur');
await blur('[data-test-field="custom-excerpt"]');
expect(
server.db.posts.find(post.id).customExcerpt,
this.server.db.posts.find(post.id).customExcerpt,
'saved excerpt'
).to.equal('Testing excerpt');
@ -640,50 +640,54 @@ describe('Acceptance: Editor', function () {
await click('[data-test-button="codeinjection"]');
// header injection has validation
let headerCM = find('[data-test-field="codeinjection-head"] .CodeMirror')[0].CodeMirror;
let headerCM = find('[data-test-field="codeinjection-head"] .CodeMirror').CodeMirror;
await headerCM.setValue(Array(65540).join('a'));
await triggerEvent(headerCM.getInputField(), 'blur');
await click(headerCM.getInputField());
await blur(headerCM.getInputField());
expect(
find('[data-test-error="codeinjection-head"]').text().trim(),
find('[data-test-error="codeinjection-head"]').textContent.trim(),
'header injection too long error'
).to.match(/cannot be longer than 65535/);
expect(
server.db.posts.find(post.id).codeinjectionHead,
this.server.db.posts.find(post.id).codeinjectionHead,
'saved header injection after validation error'
).to.be.null;
// changing header injection auto-saves
await headerCM.setValue('<script src="http://example.com/inject-head.js"></script>');
await triggerEvent(headerCM.getInputField(), 'blur');
await click(headerCM.getInputField());
await blur(headerCM.getInputField());
expect(
server.db.posts.find(post.id).codeinjectionHead,
this.server.db.posts.find(post.id).codeinjectionHead,
'saved header injection'
).to.equal('<script src="http://example.com/inject-head.js"></script>');
// footer injection has validation
let footerCM = find('[data-test-field="codeinjection-foot"] .CodeMirror')[0].CodeMirror;
let footerCM = find('[data-test-field="codeinjection-foot"] .CodeMirror').CodeMirror;
await footerCM.setValue(Array(65540).join('a'));
await triggerEvent(footerCM.getInputField(), 'blur');
await click(footerCM.getInputField());
await blur(footerCM.getInputField());
expect(
find('[data-test-error="codeinjection-foot"]').text().trim(),
find('[data-test-error="codeinjection-foot"]').textContent.trim(),
'footer injection too long error'
).to.match(/cannot be longer than 65535/);
expect(
server.db.posts.find(post.id).codeinjectionFoot,
this.server.db.posts.find(post.id).codeinjectionFoot,
'saved footer injection after validation error'
).to.be.null;
// changing footer injection auto-saves
await footerCM.setValue('<script src="http://example.com/inject-foot.js"></script>');
await triggerEvent(footerCM.getInputField(), 'blur');
await click(footerCM.getInputField());
await blur(footerCM.getInputField());
expect(
server.db.posts.find(post.id).codeinjectionFoot,
this.server.db.posts.find(post.id).codeinjectionFoot,
'saved footer injection'
).to.equal('<script src="http://example.com/inject-foot.js"></script>');
@ -691,7 +695,7 @@ describe('Acceptance: Editor', function () {
await click('[data-test-button="close-psm-subview"]');
expect(
find('[data-test-field="codeinjection-head"]').length,
findAll('[data-test-field="codeinjection-head"]').length,
'header injection not present after closing subview'
).to.equal(0);
@ -701,50 +705,54 @@ describe('Acceptance: Editor', function () {
await click('[data-test-button="twitter-data"]');
// twitter title has validation
await click('[data-test-field="twitter-title"]');
await fillIn('[data-test-field="twitter-title"]', Array(302).join('a'));
await triggerEvent('[data-test-field="twitter-title"]', 'blur');
await blur('[data-test-field="twitter-title"]');
expect(
find('[data-test-error="twitter-title"]').text().trim(),
find('[data-test-error="twitter-title"]').textContent.trim(),
'twitter title too long error'
).to.match(/cannot be longer than 300/);
expect(
server.db.posts.find(post.id).twitterTitle,
this.server.db.posts.find(post.id).twitterTitle,
'saved twitter title after validation error'
).to.be.null;
// changing twitter title auto-saves
// twitter title has validation
await click('[data-test-field="twitter-title"]');
await fillIn('[data-test-field="twitter-title"]', 'Test Twitter Title');
await triggerEvent('[data-test-field="twitter-title"]', 'blur');
await blur('[data-test-field="twitter-title"]');
expect(
server.db.posts.find(post.id).twitterTitle,
this.server.db.posts.find(post.id).twitterTitle,
'saved twitter title'
).to.equal('Test Twitter Title');
// twitter description has validation
await click('[data-test-field="twitter-description"]');
await fillIn('[data-test-field="twitter-description"]', Array(505).join('a'));
await triggerEvent('[data-test-field="twitter-description"]', 'blur');
await blur('[data-test-field="twitter-description"]');
expect(
find('[data-test-error="twitter-description"]').text().trim(),
find('[data-test-error="twitter-description"]').textContent.trim(),
'twitter description too long error'
).to.match(/cannot be longer than 500/);
expect(
server.db.posts.find(post.id).twitterDescription,
this.server.db.posts.find(post.id).twitterDescription,
'saved twitter description after validation error'
).to.be.null;
// changing twitter description auto-saves
// twitter description has validation
await click('[data-test-field="twitter-description"]');
await fillIn('[data-test-field="twitter-description"]', 'Test Twitter Description');
await triggerEvent('[data-test-field="twitter-description"]', 'blur');
await blur('[data-test-field="twitter-description"]');
expect(
server.db.posts.find(post.id).twitterDescription,
this.server.db.posts.find(post.id).twitterDescription,
'saved twitter description'
).to.equal('Test Twitter Description');
@ -752,7 +760,7 @@ describe('Acceptance: Editor', function () {
await click('[data-test-button="close-psm-subview"]');
expect(
find('[data-test-field="twitter-title"]').length,
findAll('[data-test-field="twitter-title"]').length,
'twitter title not present after closing subview'
).to.equal(0);
@ -762,50 +770,54 @@ describe('Acceptance: Editor', function () {
await click('[data-test-button="facebook-data"]');
// facebook title has validation
await click('[data-test-field="og-title"]');
await fillIn('[data-test-field="og-title"]', Array(302).join('a'));
await triggerEvent('[data-test-field="og-title"]', 'blur');
await blur('[data-test-field="og-title"]');
expect(
find('[data-test-error="og-title"]').text().trim(),
find('[data-test-error="og-title"]').textContent.trim(),
'facebook title too long error'
).to.match(/cannot be longer than 300/);
expect(
server.db.posts.find(post.id).ogTitle,
this.server.db.posts.find(post.id).ogTitle,
'saved facebook title after validation error'
).to.be.null;
// changing facebook title auto-saves
// facebook title has validation
await click('[data-test-field="og-title"]');
await fillIn('[data-test-field="og-title"]', 'Test Facebook Title');
await triggerEvent('[data-test-field="og-title"]', 'blur');
await blur('[data-test-field="og-title"]');
expect(
server.db.posts.find(post.id).ogTitle,
this.server.db.posts.find(post.id).ogTitle,
'saved facebook title'
).to.equal('Test Facebook Title');
// facebook description has validation
await click('[data-test-field="og-description"]');
await fillIn('[data-test-field="og-description"]', Array(505).join('a'));
await triggerEvent('[data-test-field="og-description"]', 'blur');
await blur('[data-test-field="og-description"]');
expect(
find('[data-test-error="og-description"]').text().trim(),
find('[data-test-error="og-description"]').textContent.trim(),
'facebook description too long error'
).to.match(/cannot be longer than 500/);
expect(
server.db.posts.find(post.id).ogDescription,
this.server.db.posts.find(post.id).ogDescription,
'saved facebook description after validation error'
).to.be.null;
// changing facebook description auto-saves
// facebook description has validation
await click('[data-test-field="og-description"]');
await fillIn('[data-test-field="og-description"]', 'Test Facebook Description');
await triggerEvent('[data-test-field="og-description"]', 'blur');
await blur('[data-test-field="og-description"]');
expect(
server.db.posts.find(post.id).ogDescription,
this.server.db.posts.find(post.id).ogDescription,
'saved facebook description'
).to.equal('Test Facebook Description');
@ -813,7 +825,7 @@ describe('Acceptance: Editor', function () {
await click('[data-test-button="close-psm-subview"]');
expect(
find('[data-test-field="og-title"]').length,
findAll('[data-test-field="og-title"]').length,
'facebook title not present after closing subview'
).to.equal(0);
});

View file

@ -1,9 +1,11 @@
import Mirage from 'ember-cli-mirage';
import destroyApp from '../helpers/destroy-app';
import startApp from '../helpers/start-app';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {click, currentRouteName, fillIn, find, findAll, visit} from '@ember/test-helpers';
import {expect} from 'chai';
import {fileUpload} from '../helpers/file-upload';
import {setupApplicationTest} from 'ember-mocha';
import {versionMismatchResponse} from 'ghost-admin/mirage/utils';
let htmlErrorResponse = function () {
@ -15,30 +17,23 @@ let htmlErrorResponse = function () {
};
describe('Acceptance: Error Handling', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
describe('VersionMismatch errors', function () {
describe('logged in', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it('displays an alert and disables navigation when saving', async function () {
server.createList('post', 3);
this.server.createList('post', 3);
// mock the post save endpoint to return version mismatch
server.put('/posts/:id', versionMismatchResponse);
this.server.put('/posts/:id', versionMismatchResponse);
await visit('/');
await click('.posts-list li:nth-of-type(2) a'); // select second post
@ -46,61 +41,61 @@ describe('Acceptance: Error Handling', function () {
await click('[data-test-publishmenu-save]'); // "Save post"
// has the refresh to update alert
expect(find('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').text()).to.match(/refresh/);
expect(findAll('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').textContent).to.match(/refresh/);
// try navigating back to the content list
await click('[data-test-link="stories"]');
expect(currentPath()).to.equal('editor.edit');
expect(currentRouteName()).to.equal('editor.edit');
});
it('displays alert and aborts the transition when navigating', async function () {
await visit('/');
// mock the tags endpoint to return version mismatch
server.get('/tags/', versionMismatchResponse);
this.server.get('/tags/', versionMismatchResponse);
await click('[data-test-nav="tags"]');
// navigation is blocked on loading screen
expect(currentPath()).to.equal('settings.tags_loading');
expect(currentRouteName()).to.equal('settings.tags_loading');
// has the refresh to update alert
expect(find('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').text()).to.match(/refresh/);
expect(findAll('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').textContent).to.match(/refresh/);
});
it('displays alert and aborts the transition when an ember-ajax error is thrown whilst navigating', async function () {
server.get('/configuration/timezones/', versionMismatchResponse);
this.server.get('/configuration/timezones/', versionMismatchResponse);
await visit('/settings/tags');
await click('[data-test-nav="settings"]');
// navigation is blocked
expect(currentPath()).to.equal('settings.general_loading');
expect(currentRouteName()).to.equal('settings.general_loading');
// has the refresh to update alert
expect(find('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').text()).to.match(/refresh/);
expect(findAll('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').textContent).to.match(/refresh/);
});
it('can be triggered when passed in to a component', async function () {
server.post('/subscribers/csv/', versionMismatchResponse);
this.server.post('/subscribers/csv/', versionMismatchResponse);
await visit('/subscribers');
await click('[data-test-link="import-csv"]');
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'test.csv'});
// alert is shown
expect(find('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').text()).to.match(/refresh/);
expect(findAll('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').textContent).to.match(/refresh/);
});
});
describe('logged out', function () {
it('displays alert', async function () {
server.post('/session', versionMismatchResponse);
this.server.post('/session', versionMismatchResponse);
await visit('/signin');
await fillIn('[name="identification"]', 'test@example.com');
@ -108,49 +103,45 @@ describe('Acceptance: Error Handling', function () {
await click('.gh-btn-blue');
// has the refresh to update alert
expect(find('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').text()).to.match(/refresh/);
expect(findAll('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').textContent).to.match(/refresh/);
});
});
});
describe('CloudFlare errors', function () {
beforeEach(function () {
let [role] = server.db.roles.where({name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
this.server.loadFixtures();
server.loadFixtures();
let roles = this.server.schema.roles.where({name: 'Administrator'});
this.server.create('user', {roles});
authenticateSession(application);
return await authenticateSession();
});
it('handles Ember Data HTML response', async function () {
server.put('/posts/1/', htmlErrorResponse);
server.create('post');
this.server.put('/posts/1/', htmlErrorResponse);
this.server.create('post');
await visit('/editor/1');
await click('[data-test-publishmenu-trigger]');
await click('[data-test-publishmenu-save]');
andThen(() => {
expect(find('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').text()).to.not.match(/html>/);
expect(find('.gh-alert').text()).to.match(/Request was rejected due to server error/);
});
expect(findAll('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').textContent).to.not.match(/html>/);
expect(find('.gh-alert').textContent).to.match(/Request was rejected due to server error/);
});
it('handles ember-ajax HTML response', async function () {
server.del('/themes/foo/', htmlErrorResponse);
this.server.del('/themes/foo/', htmlErrorResponse);
await visit('/settings/design');
await click('[data-test-theme-id="foo"] [data-test-theme-delete-button]');
await click('.fullscreen-modal [data-test-delete-button]');
andThen(() => {
expect(find('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').text()).to.not.match(/html>/);
expect(find('.gh-alert').text()).to.match(/Request was rejected due to server error/);
});
expect(findAll('.gh-alert').length).to.equal(1);
expect(find('.gh-alert').textContent).to.not.match(/html>/);
expect(find('.gh-alert').textContent).to.match(/Request was rejected due to server error/);
});
});
});

View file

@ -1,27 +1,23 @@
import destroyApp from '../helpers/destroy-app';
import startApp from '../helpers/start-app';
import {afterEach, beforeEach, describe, it} from 'mocha';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {click, fillIn, find, findAll, visit} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {invalidateSession} from 'ember-simple-auth/test-support';
import {setupApplicationTest} from 'ember-mocha';
describe('Acceptance: Password Reset', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
describe('request reset', function () {
it('is successful with valid data', async function () {
await invalidateSession();
await visit('/signin');
await fillIn('input[name="identification"]', 'test@example.com');
await click('.forgotten-link');
// an alert with instructions is displayed
expect(find('.gh-alert-blue').length, 'alert count')
expect(findAll('.gh-alert-blue').length, 'alert count')
.to.equal(1);
});
@ -33,18 +29,18 @@ describe('Acceptance: Password Reset', function () {
// email field is invalid
expect(
find('input[name="identification"]').closest('.form-group').hasClass('error'),
find('input[name="identification"]').closest('.form-group'),
'email field has error class (no email)'
).to.be.true;
).to.match('.error');
// password field is valid
expect(
find('input[name="password"]').closest('.form-group').hasClass('error'),
find('input[name="password"]').closest('.form-group'),
'password field has error class (no email)'
).to.be.false;
).to.not.match('.error');
// error message shown
expect(find('p.main-error').text().trim(), 'error message')
expect(find('p.main-error').textContent.trim(), 'error message')
.to.equal('We need your email address to reset your password!');
// invalid email provided
@ -53,18 +49,18 @@ describe('Acceptance: Password Reset', function () {
// email field is invalid
expect(
find('input[name="identification"]').closest('.form-group').hasClass('error'),
find('input[name="identification"]').closest('.form-group'),
'email field has error class (invalid email)'
).to.be.true;
).to.match('.error');
// password field is valid
expect(
find('input[name="password"]').closest('.form-group').hasClass('error'),
find('input[name="password"]').closest('.form-group'),
'password field has error class (invalid email)'
).to.be.false;
).to.not.match('.error');
// error message
expect(find('p.main-error').text().trim(), 'error message')
expect(find('p.main-error').textContent.trim(), 'error message')
.to.equal('We need your email address to reset your password!');
// unknown email provided
@ -73,18 +69,18 @@ describe('Acceptance: Password Reset', function () {
// email field is invalid
expect(
find('input[name="identification"]').closest('.form-group').hasClass('error'),
find('input[name="identification"]').closest('.form-group'),
'email field has error class (unknown email)'
).to.be.true;
).to.match('.error');
// password field is valid
expect(
find('input[name="password"]').closest('.form-group').hasClass('error'),
find('input[name="password"]').closest('.form-group'),
'password field has error class (unknown email)'
).to.be.false;
).to.not.match('.error');
// error message
expect(find('p.main-error').text().trim(), 'error message')
expect(find('p.main-error').textContent.trim(), 'error message')
.to.equal('There is no user with that email address.');
});
});

View file

@ -1,69 +1,63 @@
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import destroyApp from '../../helpers/destroy-app';
import startApp from '../../helpers/start-app';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {
afterEach,
beforeEach,
describe,
it
} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {click, currentURL, find, findAll, triggerEvent} from '@ember/test-helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../../helpers/visit';
describe('Acceptance: Settings - Integrations - AMP', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/integrations/amp');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('redirects to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/amp');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/amp');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/amp');
expect(currentURL(), 'currentURL').to.equal('/team');
});
describe('when logged in', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it('it enables or disables AMP properly and saves it', async function () {
@ -73,15 +67,15 @@ describe('Acceptance: Settings - Integrations - AMP', function () {
expect(currentURL(), 'currentURL').to.equal('/settings/integrations/amp');
// AMP is enabled by default
expect(find('[data-test-amp-checkbox]').prop('checked'), 'AMP checkbox').to.be.true;
expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox').to.be.true;
await click('[data-test-amp-checkbox]');
expect(find('[data-test-amp-checkbox]').prop('checked'), 'AMP checkbox').to.be.false;
expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox').to.be.false;
await click('[data-test-save-button]');
let [lastRequest] = server.pretender.handledRequests.slice(-1);
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
let params = JSON.parse(lastRequest.requestBody);
expect(params.settings.findBy('key', 'amp').value).to.equal(false);
@ -96,10 +90,10 @@ describe('Acceptance: Settings - Integrations - AMP', function () {
// we've already saved in this test so there's no on-screen indication
// that we've had another save, check the request was fired instead
let [newRequest] = server.pretender.handledRequests.slice(-1);
let [newRequest] = this.server.pretender.handledRequests.slice(-1);
params = JSON.parse(newRequest.requestBody);
expect(find('[data-test-amp-checkbox]').prop('checked'), 'AMP checkbox').to.be.true;
expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox').to.be.true;
expect(params.settings.findBy('key', 'amp').value).to.equal(true);
});
@ -110,27 +104,27 @@ describe('Acceptance: Settings - Integrations - AMP', function () {
expect(currentURL(), 'currentURL').to.equal('/settings/integrations/amp');
// AMP is enabled by default
expect(find('[data-test-amp-checkbox]').prop('checked'), 'AMP checkbox').to.be.true;
expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox default').to.be.true;
await click('[data-test-amp-checkbox]');
expect(find('[data-test-amp-checkbox]').prop('checked'), 'AMP checkbox').to.be.false;
expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox after click').to.be.false;
await visit('/team');
expect(find('.fullscreen-modal').length, 'modal exists').to.equal(1);
expect(findAll('.fullscreen-modal').length, 'unsaved changes modal exists').to.equal(1);
// Leave without saving
await (click('.fullscreen-modal [data-test-leave-button]'), 'leave without saving');
await click('.fullscreen-modal [data-test-leave-button]');
expect(currentURL(), 'currentURL').to.equal('/team');
expect(currentURL(), 'currentURL after leave without saving').to.equal('/team');
await visit('/settings/integrations/amp');
expect(currentURL(), 'currentURL').to.equal('/settings/integrations/amp');
expect(currentURL(), 'currentURL after return').to.equal('/settings/integrations/amp');
// settings were not saved
expect(find('[data-test-amp-checkbox]').prop('checked'), 'AMP checkbox').to.be.true;
expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox').to.be.true;
});
});
});

View file

@ -1,70 +1,63 @@
import $ from 'jquery';
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import destroyApp from '../../helpers/destroy-app';
import startApp from '../../helpers/start-app';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {
afterEach,
beforeEach,
describe,
it
} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {click, currentURL, find, findAll, triggerEvent} from '@ember/test-helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../../helpers/visit';
describe('Acceptance: Settings - Code-Injection', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/code-injection');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('redirects to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/code-injection');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/code-injection');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/code-injection');
expect(currentURL(), 'currentURL').to.equal('/team');
});
describe('when logged in', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it('it renders, loads and saves editors correctly', async function () {
@ -77,24 +70,24 @@ describe('Acceptance: Settings - Code-Injection', function () {
expect(document.title, 'page title').to.equal('Settings - Code injection - Test Blog');
// highlights nav menu
expect($('[data-test-nav="code-injection"]').hasClass('active'), 'highlights nav menu item')
.to.be.true;
expect(find('[data-test-nav="code-injection"]'), 'highlights nav menu item')
.to.have.class('active');
expect(find('[data-test-save-button]').text().trim(), 'save button text').to.equal('Save');
expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Save');
expect(find('#ghost-head .CodeMirror').length, 'ghost head codemirror element').to.equal(1);
expect($('#ghost-head .CodeMirror').hasClass('cm-s-xq-light'), 'ghost head editor theme').to.be.true;
expect(findAll('#ghost-head .CodeMirror').length, 'ghost head codemirror element').to.equal(1);
expect(find('#ghost-head .CodeMirror'), 'ghost head editor theme').to.have.class('cm-s-xq-light');
expect(find('#ghost-foot .CodeMirror').length, 'ghost head codemirror element').to.equal(1);
expect($('#ghost-foot .CodeMirror').hasClass('cm-s-xq-light'), 'ghost head editor theme').to.be.true;
expect(findAll('#ghost-foot .CodeMirror').length, 'ghost head codemirror element').to.equal(1);
expect(find('#ghost-foot .CodeMirror'), 'ghost head editor theme').to.have.class('cm-s-xq-light');
await click('[data-test-save-button]');
let [lastRequest] = server.pretender.handledRequests.slice(-1);
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
let params = JSON.parse(lastRequest.requestBody);
expect(params.settings.findBy('key', 'ghost_head').value).to.equal('');
expect(find('[data-test-save-button]').text().trim(), 'save button text').to.equal('Saved');
expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Saved');
// CMD-S shortcut works
await triggerEvent('.gh-app', 'keydown', {
@ -104,11 +97,11 @@ describe('Acceptance: Settings - Code-Injection', function () {
});
// we've already saved in this test so there's no on-screen indication
// that we've had another save, check the request was fired instead
let [newRequest] = server.pretender.handledRequests.slice(-1);
let [newRequest] = this.server.pretender.handledRequests.slice(-1);
params = JSON.parse(newRequest.requestBody);
expect(params.settings.findBy('key', 'ghost_head').value).to.equal('');
expect(find('[data-test-save-button]').text().trim(), 'save button text').to.equal('Saved');
expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Saved');
});
});
});

View file

@ -1,141 +1,136 @@
/* eslint-disable camelcase */
import Mirage from 'ember-cli-mirage';
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import destroyApp from '../../helpers/destroy-app';
import mockThemes from 'ghost-admin/mirage/config/themes';
import startApp from '../../helpers/start-app';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {blur, click, currentRouteName, currentURL, fillIn, find, findAll, triggerEvent, typeIn} from '@ember/test-helpers';
import {expect} from 'chai';
import {fileUpload} from '../../helpers/file-upload';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../../helpers/visit';
// simulate jQuery's `:visible` pseudo-selector
function withText(elements) {
return Array.from(elements).filter(elem => elem.textContent.trim() !== '');
}
describe('Acceptance: Settings - Design', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/design');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('redirects to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/design');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/design');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
describe('when logged in', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
authenticateSession(application);
await authenticateSession();
});
it('can visit /settings/design', async function () {
await visit('/settings/design');
expect(currentPath()).to.equal('settings.design.index');
expect(find('[data-test-save-button]').text().trim(), 'save button text').to.equal('Save');
expect(currentRouteName()).to.equal('settings.design.index');
expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Save');
// fixtures contain two nav items, check for three rows as we
// should have one extra that's blank
expect(
find('.gh-blognav-item').length,
findAll('[data-test-navitem]').length,
'navigation items count'
).to.equal(3);
});
it('saves navigation settings', async function () {
await visit('/settings/design');
await fillIn('.gh-blognav-label:first input', 'Test');
await fillIn('.gh-blognav-url:first input', '/test');
await triggerEvent('.gh-blognav-url:first input', 'blur');
await fillIn('[data-test-navitem="0"] [data-test-input="label"]', 'Test');
await typeIn('[data-test-navitem="0"] [data-test-input="url"]', '/test');
await click('[data-test-save-button]');
let [navSetting] = server.db.settings.where({key: 'navigation'});
let [navSetting] = this.server.db.settings.where({key: 'navigation'});
expect(navSetting.value).to.equal('[{"label":"Test","url":"/test/"},{"label":"About","url":"/about"}]');
// don't test against .error directly as it will pick up failed
// tests "pre.error" elements
expect(find('span.error').length, 'error fields count').to.equal(0);
expect(find('.gh-alert').length, 'alerts count').to.equal(0);
expect(find('.response:visible').length, 'validation errors count')
expect(findAll('span.error').length, 'error messages count').to.equal(0);
expect(findAll('.gh-alert').length, 'alerts count').to.equal(0);
expect(withText(findAll('[data-test-error]')).length, 'validation errors count')
.to.equal(0);
});
it('validates new item correctly on save', async function () {
await visit('/settings/design');
await click('[data-test-save-button]');
expect(
find('.gh-blognav-item').length,
findAll('[data-test-navitem]').length,
'number of nav items after saving with blank new item'
).to.equal(3);
await fillIn('.gh-blognav-label:last input', 'Test');
await fillIn('.gh-blognav-url:last input', 'http://invalid domain/');
await triggerEvent('.gh-blognav-url:last input', 'blur');
await fillIn('[data-test-navitem="new"] [data-test-input="label"]', 'Test');
await fillIn('[data-test-navitem="new"] [data-test-input="url"]', '');
await typeIn('[data-test-navitem="new"] [data-test-input="url"]', 'http://invalid domain/');
await click('[data-test-save-button]');
expect(
find('.gh-blognav-item').length,
findAll('[data-test-navitem]').length,
'number of nav items after saving with invalid new item'
).to.equal(3);
expect(
find('.gh-blognav-item:last .error').length,
withText(findAll('[data-test-navitem="new"] [data-test-error]')).length,
'number of invalid fields in new item'
).to.equal(1);
});
it('clears unsaved settings when navigating away but warns with a confirmation dialog', async function () {
await visit('/settings/design');
await fillIn('.gh-blognav-label:first input', 'Test');
await triggerEvent('.gh-blognav-label:first input', 'blur');
await fillIn('[data-test-navitem="0"] [data-test-input="label"]', 'Test');
await blur('[data-test-navitem="0"] [data-test-input="label"]');
expect(find('.gh-blognav-label:first input').val()).to.equal('Test');
// this.timeout(0);
// return pauseTest();
expect(find('[data-test-navitem="0"] [data-test-input="label"]').value).to.equal('Test');
await visit('/settings/code-injection');
expect(find('.fullscreen-modal').length, 'modal exists').to.equal(1);
expect(findAll('.fullscreen-modal').length, 'modal exists').to.equal(1);
// Leave without saving
await (click('.fullscreen-modal [data-test-leave-button]'), 'leave without saving');
await click('.fullscreen-modal [data-test-leave-button]'), 'leave without saving';
expect(currentURL(), 'currentURL').to.equal('/settings/code-injection');
await visit('/settings/design');
expect(find('.gh-blognav-label:first input').val()).to.equal('Home');
expect(find('[data-test-navitem="0"] [data-test-input="label"]').value).to.equal('Home');
});
it('can add and remove items', async function () {
@ -143,57 +138,57 @@ describe('Acceptance: Settings - Design', function () {
await click('.gh-blognav-add');
expect(
find('.gh-blognav-label:last .response').is(':visible'),
find('[data-test-navitem="new"] [data-test-error="label"]').textContent.trim(),
'blank label has validation error'
).to.be.true;
).to.not.be.empty;
await fillIn('.gh-blognav-label:last input', 'New');
await triggerEvent('.gh-blognav-label:last input', 'keypress', {});
await fillIn('[data-test-navitem="new"] [data-test-input="label"]', '');
await typeIn('[data-test-navitem="new"] [data-test-input="label"]', 'New');
expect(
find('.gh-blognav-label:last .response').is(':visible'),
find('[data-test-navitem="new"] [data-test-error="label"]').textContent.trim(),
'label validation is visible after typing'
).to.be.false;
).to.be.empty;
await fillIn('.gh-blognav-url:last input', '/new');
await triggerEvent('.gh-blognav-url:last input', 'keypress', {});
await triggerEvent('.gh-blognav-url:last input', 'blur');
await fillIn('[data-test-navitem="new"] [data-test-input="url"]', '');
await typeIn('[data-test-navitem="new"] [data-test-input="url"]', '/new');
await blur('[data-test-navitem="new"] [data-test-input="url"]');
expect(
find('.gh-blognav-url:last .response').is(':visible'),
find('[data-test-navitem="new"] [data-test-error="url"]').textContent.trim(),
'url validation is visible after typing'
).to.be.false;
).to.be.empty;
expect(
find('.gh-blognav-url:last input').val()
).to.equal(`${window.location.origin}/new`);
find('[data-test-navitem="new"] [data-test-input="url"]').value
).to.equal(`${window.location.origin}/new/`);
await click('.gh-blognav-add');
expect(
find('.gh-blognav-item').length,
findAll('[data-test-navitem]').length,
'number of nav items after successful add'
).to.equal(4);
expect(
find('.gh-blognav-label:last input').val(),
find('[data-test-navitem="new"] [data-test-input="label"]').value,
'new item label value after successful add'
).to.be.empty;
expect(
find('.gh-blognav-url:last input').val(),
find('[data-test-navitem="new"] [data-test-input="url"]').value,
'new item url value after successful add'
).to.equal(`${window.location.origin}/`);
expect(
find('.gh-blognav-item .response:visible').length,
withText(findAll('[data-test-navitem] [data-test-error]')).length,
'number or validation errors shown after successful add'
).to.equal(0);
await click('.gh-blognav-item:first .gh-blognav-delete');
await click('[data-test-navitem="0"] .gh-blognav-delete');
expect(
find('.gh-blognav-item').length,
findAll('[data-test-navitem]').length,
'number of nav items after successful remove'
).to.equal(3);
@ -204,7 +199,7 @@ describe('Acceptance: Settings - Design', function () {
ctrlKey: ctrlOrCmd === 'ctrl'
});
let [navSetting] = server.db.settings.where({key: 'navigation'});
let [navSetting] = this.server.db.settings.where({key: 'navigation'});
expect(navSetting.value).to.equal('[{"label":"About","url":"/about"},{"label":"New","url":"/new/"}]');
});
@ -228,39 +223,40 @@ describe('Acceptance: Settings - Design', function () {
// - displays modal
// - deletes theme and refreshes list
server.loadFixtures('themes');
this.server.loadFixtures('themes');
await visit('/settings/design');
// lists available themes (themes are specified in mirage/fixtures/settings)
expect(
find('[data-test-theme-id]').length,
findAll('[data-test-theme-id]').length,
'shows correct number of themes'
).to.equal(3);
expect(
find('[data-test-theme-active="true"] [data-test-theme-title]').text().trim(),
find('[data-test-theme-active="true"] [data-test-theme-title]').textContent.trim(),
'Blog theme marked as active'
).to.equal('Blog (default)');
// theme upload displays modal
await click('[data-test-upload-theme-button]');
expect(
find('[data-test-modal="upload-theme"]').length,
findAll('[data-test-modal="upload-theme"]').length,
'theme upload modal displayed after button click'
).to.equal(1);
// cancelling theme upload closes modal
await click('.fullscreen-modal [data-test-close-button]');
expect(
find('.fullscreen-modal').length === 0,
findAll('.fullscreen-modal').length === 0,
'upload theme modal is closed when cancelling'
).to.be.true;
// theme upload validates mime type
await click('[data-test-upload-theme-button]');
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {type: 'text/csv'});
expect(
find('.fullscreen-modal .failed').text(),
find('.fullscreen-modal .failed').textContent,
'validation error is shown for invalid mime type'
).to.match(/is not supported/);
@ -268,12 +264,12 @@ describe('Acceptance: Settings - Design', function () {
await click('[data-test-upload-try-again-button]');
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'casper.zip', type: 'application/zip'});
expect(
find('.fullscreen-modal .failed').text(),
find('.fullscreen-modal .failed').textContent,
'validation error is shown when uploading casper.zip'
).to.match(/default Casper theme cannot be overwritten/);
// theme upload handles upload errors
server.post('/themes/upload/', function () {
this.server.post('/themes/upload/', function () {
return new Mirage.Response(422, {}, {
errors: [{
message: 'Invalid theme'
@ -282,16 +278,17 @@ describe('Acceptance: Settings - Design', function () {
});
await click('[data-test-upload-try-again-button]');
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'error.zip', type: 'application/zip'});
expect(
find('.fullscreen-modal .failed').text().trim(),
find('.fullscreen-modal .failed').textContent.trim(),
'validation error is passed through from server'
).to.equal('Invalid theme');
// reset to default mirage handlers
mockThemes(server);
mockThemes(this.server);
// theme upload handles validation errors
server.post('/themes/upload/', function () {
this.server.post('/themes/upload/', function () {
return new Mirage.Response(422, {}, {
errors: [
{
@ -332,48 +329,48 @@ describe('Acceptance: Settings - Design', function () {
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'bad-theme.zip', type: 'application/zip'});
expect(
find('.fullscreen-modal h1').text().trim(),
find('.fullscreen-modal h1').textContent.trim(),
'modal title after uploading invalid theme'
).to.equal('Invalid theme');
expect(
find('.theme-validation-rule-text').text(),
findAll('.theme-validation-rule-text')[1].textContent,
'top-level errors are displayed'
).to.match(/Templates must contain valid Handlebars/);
await click('[data-test-toggle-details]');
expect(
find('.theme-validation-details').text(),
find('.theme-validation-details').textContent,
'top-level errors do not escape HTML'
).to.match(/The listed files should be included using the {{asset}} helper/);
expect(
find('.theme-validation-list ul li').text(),
find('.theme-validation-list ul li').textContent,
'individual failures are displayed'
).to.match(/\/assets\/javascripts\/ui\.js/);
// reset to default mirage handlers
mockThemes(server);
mockThemes(this.server);
await click('.fullscreen-modal [data-test-try-again-button]');
expect(
find('.theme-validation-errors').length,
findAll('.theme-validation-errors').length,
'"Try Again" resets form after theme validation error'
).to.equal(0);
expect(
find('.gh-image-uploader').length,
findAll('.gh-image-uploader').length,
'"Try Again" resets form after theme validation error'
).to.equal(1);
expect(
find('.fullscreen-modal h1').text().trim(),
find('.fullscreen-modal h1').textContent.trim(),
'"Try Again" resets form after theme validation error'
).to.equal('Upload a theme');
// theme upload handles validation warnings
server.post('/themes/upload/', function ({themes}) {
this.server.post('/themes/upload/', function ({themes}) {
let theme = {
name: 'blackpalm',
package: {
@ -413,24 +410,24 @@ describe('Acceptance: Settings - Design', function () {
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'warning-theme.zip', type: 'application/zip'});
expect(
find('.fullscreen-modal h1').text().trim(),
find('.fullscreen-modal h1').textContent.trim(),
'modal title after uploading theme with warnings'
).to.equal('Upload successful with warnings');
await click('[data-test-toggle-details]');
expect(
find('.theme-validation-details').text(),
find('.theme-validation-details').textContent,
'top-level warnings are displayed'
).to.match(/The listed files should be included using the {{asset}} helper/);
expect(
find('.theme-validation-list ul li').text(),
find('.theme-validation-list ul li').textContent,
'individual warning failures are displayed'
).to.match(/\/assets\/dist\/img\/apple-touch-icon\.png/);
// reset to default mirage handlers
mockThemes(server);
mockThemes(this.server);
await click('.fullscreen-modal [data-test-close-button]');
@ -439,22 +436,22 @@ describe('Acceptance: Settings - Design', function () {
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'theme-1.zip', type: 'application/zip'});
expect(
find('.fullscreen-modal h1').text().trim(),
find('.fullscreen-modal h1').textContent.trim(),
'modal header after successful upload'
).to.equal('Upload successful!');
expect(
find('.modal-body').text(),
find('.modal-body').textContent,
'modal displays theme name after successful upload'
).to.match(/"Test 1 - 0\.1" uploaded successfully/);
expect(
find('[data-test-theme-id]').length,
findAll('[data-test-theme-id]').length,
'number of themes in list grows after upload'
).to.equal(5);
expect(
find('[data-test-theme-active="true"] [data-test-theme-title]').text().trim(),
find('[data-test-theme-active="true"] [data-test-theme-title]').textContent.trim(),
'newly uploaded theme is not active'
).to.equal('Blog (default)');
@ -466,12 +463,12 @@ describe('Acceptance: Settings - Design', function () {
await click('.fullscreen-modal [data-test-activate-now-button]');
expect(
find('[data-test-theme-id]').length,
findAll('[data-test-theme-id]').length,
'number of themes in list grows after upload and activate'
).to.equal(6);
expect(
find('[data-test-theme-active="true"] [data-test-theme-title]').text().trim(),
find('[data-test-theme-active="true"] [data-test-theme-title]').textContent.trim(),
'newly uploaded+activated theme is active'
).to.equal('Test 2');
@ -479,17 +476,17 @@ describe('Acceptance: Settings - Design', function () {
await click('[data-test-theme-id="casper"] [data-test-theme-activate-button]');
expect(
find('[data-test-theme-id="test-2"] .apps-card-app').hasClass('theme-list-item--active'),
find('[data-test-theme-id="test-2"] .apps-card-app').classList.contains('theme-list-item--active'),
'previously active theme is not active'
).to.be.false;
expect(
find('[data-test-theme-id="casper"] .apps-card-app').hasClass('theme-list-item--active'),
find('[data-test-theme-id="casper"] .apps-card-app').classList.contains('theme-list-item--active'),
'activated theme is active'
).to.be.true;
// theme activation shows errors
server.put('themes/:theme/activate', function () {
this.server.put('themes/:theme/activate', function () {
return new Mirage.Response(422, {}, {
errors: [
{
@ -531,35 +528,35 @@ describe('Acceptance: Settings - Design', function () {
expect(find('[data-test-theme-warnings-modal]')).to.exist;
expect(
find('[data-test-theme-warnings-title]').text().trim(),
find('[data-test-theme-warnings-title]').textContent.trim(),
'modal title after activating invalid theme'
).to.equal('Activation failed');
expect(
find('[data-test-theme-warnings]').text(),
find('[data-test-theme-warnings]').textContent,
'top-level errors are displayed in activation errors'
).to.match(/Templates must contain valid Handlebars/);
await click('[data-test-toggle-details]');
expect(
find('.theme-validation-details').text(),
find('.theme-validation-details').textContent,
'top-level errors do not escape HTML in activation errors'
).to.match(/The listed files should be included using the {{asset}} helper/);
expect(
find('.theme-validation-list ul li').text(),
find('.theme-validation-list ul li').textContent,
'individual failures are displayed in activation errors'
).to.match(/\/assets\/javascripts\/ui\.js/);
// restore default mirage handlers
mockThemes(server);
mockThemes(this.server);
await click('[data-test-modal-close-button]');
expect(find('[data-test-theme-warnings-modal]')).to.not.exist;
// theme activation shows warnings
server.put('themes/:theme/activate', function ({themes}, {params}) {
this.server.put('themes/:theme/activate', function ({themes}, {params}) {
themes.all().update('active', false);
let theme = themes.findBy({name: params.theme}).update({active: true});
@ -592,24 +589,24 @@ describe('Acceptance: Settings - Design', function () {
expect(find('[data-test-theme-warnings-modal]')).to.exist;
expect(
find('[data-test-theme-warnings-title]').text().trim(),
find('[data-test-theme-warnings-title]').textContent.trim(),
'modal title after activating theme with warnings'
).to.equal('Activation successful with warnings');
await click('[data-test-toggle-details]');
expect(
find('.theme-validation-details').text(),
find('.theme-validation-details').textContent,
'top-level warnings are displayed in activation warnings'
).to.match(/The listed files should be included using the {{asset}} helper/);
expect(
find('.theme-validation-list ul li').text(),
find('.theme-validation-list ul li').textContent,
'individual warning failures are displayed in activation warnings'
).to.match(/\/assets\/dist\/img\/apple-touch-icon\.png/);
// restore default mirage handlers
mockThemes(server);
mockThemes(this.server);
await click('[data-test-modal-close-button]');
// reactivate casper to continue tests
@ -618,14 +615,14 @@ describe('Acceptance: Settings - Design', function () {
// theme deletion displays modal
await click('[data-test-theme-id="test-1"] [data-test-theme-delete-button]');
expect(
find('[data-test-delete-theme-modal]').length,
findAll('[data-test-delete-theme-modal]').length,
'theme deletion modal displayed after button click'
).to.equal(1);
// cancelling theme deletion closes modal
await click('.fullscreen-modal [data-test-cancel-button]');
expect(
find('.fullscreen-modal').length === 0,
findAll('.fullscreen-modal').length === 0,
'delete theme modal is closed when cancelling'
).to.be.true;
@ -633,22 +630,22 @@ describe('Acceptance: Settings - Design', function () {
await click('[data-test-theme-id="test-1"] [data-test-theme-delete-button]');
await click('.fullscreen-modal [data-test-delete-button]');
expect(
find('.fullscreen-modal').length === 0,
findAll('.fullscreen-modal').length === 0,
'delete theme modal closes after deletion'
).to.be.true;
expect(
find('[data-test-theme-id]').length,
findAll('[data-test-theme-id]').length,
'number of themes in list shrinks after delete'
).to.equal(5);
expect(
find('[data-test-theme-title]').text(),
find('[data-test-theme-title]').textContent,
'correct theme is removed from theme list after deletion'
).to.not.match(/Test 1/);
// validation errors are handled when deleting a theme
server.del('/themes/:theme/', function () {
this.server.del('/themes/:theme/', function () {
return new Mirage.Response(422, {}, {
errors: [{
message: 'Can\'t delete theme'
@ -660,29 +657,29 @@ describe('Acceptance: Settings - Design', function () {
await click('.fullscreen-modal [data-test-delete-button]');
expect(
find('.fullscreen-modal').length === 0,
findAll('.fullscreen-modal').length === 0,
'delete theme modal closes after failed deletion'
).to.be.true;
expect(
find('.gh-alert').length,
findAll('.gh-alert').length,
'alert is shown when deletion fails'
).to.equal(1);
expect(
find('.gh-alert').text(),
find('.gh-alert').textContent,
'failed deletion alert has correct text'
).to.match(/Can't delete theme/);
// restore default mirage handlers
mockThemes(server);
mockThemes(this.server);
});
it('can delete then re-upload the same theme', async function () {
server.loadFixtures('themes');
this.server.loadFixtures('themes');
// mock theme upload to emulate uploading theme with same id
server.post('/themes/upload/', function ({themes}) {
this.server.post('/themes/upload/', function ({themes}) {
let theme = themes.create({
name: 'foo',
package: {

View file

@ -1,68 +1,63 @@
import $ from 'jquery';
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import destroyApp from '../../helpers/destroy-app';
import mockUploads from '../../../mirage/config/uploads';
import startApp from '../../helpers/start-app';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import wait from 'ember-test-helpers/wait';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {blur, click, currentURL, fillIn, find, findAll, focus, triggerEvent} from '@ember/test-helpers';
import {expect} from 'chai';
import {fileUpload} from '../../helpers/file-upload';
import {run} from '@ember/runloop';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../../helpers/visit';
describe('Acceptance: Settings - General', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/general');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('redirects to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/general');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/general');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/general');
expect(currentURL(), 'currentURL').to.equal('/team');
});
describe('when logged in', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it('it renders, handles image uploads', async function () {
@ -75,11 +70,11 @@ describe('Acceptance: Settings - General', function () {
expect(document.title, 'page title').to.equal('Settings - General - Test Blog');
// highlights nav menu
expect($('[data-test-nav="settings"]').hasClass('active'), 'highlights nav menu item')
.to.be.true;
expect(find('[data-test-nav="settings"]'), 'highlights nav menu item')
.to.have.class('active');
expect(
find('[data-test-save-button]').text().trim(),
find('[data-test-save-button]').textContent.trim(),
'save button text'
).to.equal('Save settings');
@ -93,7 +88,7 @@ describe('Acceptance: Settings - General', function () {
// has fixture icon
expect(
find('[data-test-icon-img]').attr('src'),
find('[data-test-icon-img]').getAttribute('src'),
'initial icon src'
).to.equal('/content/images/2014/Feb/favicon.ico');
@ -110,7 +105,7 @@ describe('Acceptance: Settings - General', function () {
// select file
fileUpload(
'[data-test-file-input="icon"]',
'[data-test-file-input="icon"] input',
['test'],
{name: 'pub-icon.ico', type: 'image/x-icon'}
);
@ -126,7 +121,7 @@ describe('Acceptance: Settings - General', function () {
// wait for upload to finish and check image is shown
await wait();
expect(
find('[data-test-icon-img]').attr('src'),
find('[data-test-icon-img]').getAttribute('src'),
'icon img after upload'
).to.match(/pub-icon\.ico$/);
expect(
@ -135,7 +130,7 @@ describe('Acceptance: Settings - General', function () {
).to.not.exist;
// failed upload shows error
server.post('/uploads/icon/', function () {
this.server.post('/uploads/icon/', function () {
return {
errors: [{
errorType: 'ValidationError',
@ -145,24 +140,24 @@ describe('Acceptance: Settings - General', function () {
}, 422);
await click('[data-test-delete-image="icon"]');
await fileUpload(
'[data-test-file-input="icon"]',
'[data-test-file-input="icon"] input',
['test'],
{name: 'pub-icon.ico', type: 'image/x-icon'}
);
expect(
find('[data-test-error="icon"]').text().trim(),
find('[data-test-error="icon"]').textContent.trim(),
'failed icon upload message'
).to.equal('Wrong icon size');
// reset upload endpoints
mockUploads(server);
mockUploads(this.server);
// blog logo upload
// -------------------------------------------------------------- //
// has fixture icon
expect(
find('[data-test-logo-img]').attr('src'),
find('[data-test-logo-img]').getAttribute('src'),
'initial logo src'
).to.equal('/content/images/2013/Nov/logo.png');
@ -179,7 +174,7 @@ describe('Acceptance: Settings - General', function () {
// select file
fileUpload(
'[data-test-file-input="logo"]',
'[data-test-file-input="logo"] input',
['test'],
{name: 'pub-logo.png', type: 'image/png'}
);
@ -195,7 +190,7 @@ describe('Acceptance: Settings - General', function () {
// wait for upload to finish and check image is shown
await wait();
expect(
find('[data-test-logo-img]').attr('src'),
find('[data-test-logo-img]').getAttribute('src'),
'logo img after upload'
).to.match(/pub-logo\.png$/);
expect(
@ -204,7 +199,7 @@ describe('Acceptance: Settings - General', function () {
).to.not.exist;
// failed upload shows error
server.post('/uploads/', function () {
this.server.post('/uploads/', function () {
return {
errors: [{
errorType: 'ValidationError',
@ -214,24 +209,24 @@ describe('Acceptance: Settings - General', function () {
}, 422);
await click('[data-test-delete-image="logo"]');
await fileUpload(
'[data-test-file-input="logo"]',
'[data-test-file-input="logo"] input',
['test'],
{name: 'pub-logo.png', type: 'image/png'}
);
expect(
find('[data-test-error="logo"]').text().trim(),
find('[data-test-error="logo"]').textContent.trim(),
'failed logo upload message'
).to.equal('Wrong logo size');
// reset upload endpoints
mockUploads(server);
mockUploads(this.server);
// blog cover upload
// -------------------------------------------------------------- //
// has fixture icon
expect(
find('[data-test-cover-img]').attr('src'),
find('[data-test-cover-img]').getAttribute('src'),
'initial coverImage src'
).to.equal('/content/images/2014/Feb/cover.jpg');
@ -248,7 +243,7 @@ describe('Acceptance: Settings - General', function () {
// select file
fileUpload(
'[data-test-file-input="coverImage"]',
'[data-test-file-input="coverImage"] input',
['test'],
{name: 'pub-coverImage.png', type: 'image/png'}
);
@ -264,7 +259,7 @@ describe('Acceptance: Settings - General', function () {
// wait for upload to finish and check image is shown
await wait();
expect(
find('[data-test-cover-img]').attr('src'),
find('[data-test-cover-img]').getAttribute('src'),
'coverImage img after upload'
).to.match(/pub-coverImage\.png$/);
expect(
@ -273,7 +268,7 @@ describe('Acceptance: Settings - General', function () {
).to.not.exist;
// failed upload shows error
server.post('/uploads/', function () {
this.server.post('/uploads/', function () {
return {
errors: [{
errorType: 'ValidationError',
@ -283,17 +278,17 @@ describe('Acceptance: Settings - General', function () {
}, 422);
await click('[data-test-delete-image="coverImage"]');
await fileUpload(
'[data-test-file-input="coverImage"]',
'[data-test-file-input="coverImage"] input',
['test'],
{name: 'pub-coverImage.png', type: 'image/png'}
);
expect(
find('[data-test-error="coverImage"]').text().trim(),
find('[data-test-error="coverImage"]').textContent.trim(),
'failed coverImage upload message'
).to.equal('Wrong coverImage size');
// reset upload endpoints
mockUploads(server);
mockUploads(this.server);
// CMD-S shortcut works
// -------------------------------------------------------------- //
@ -305,7 +300,7 @@ describe('Acceptance: Settings - General', function () {
});
// we've already saved in this test so there's no on-screen indication
// that we've had another save, check the request was fired instead
let [lastRequest] = server.pretender.handledRequests.slice(-1);
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
let params = JSON.parse(lastRequest.requestBody);
expect(params.settings.findBy('key', 'title').value).to.equal('CMD-S Test');
});
@ -316,57 +311,57 @@ describe('Acceptance: Settings - General', function () {
expect(currentURL(), 'currentURL').to.equal('/settings/general');
expect(find('#activeTimezone option').length, 'available timezones').to.equal(66);
expect(find('#activeTimezone option:selected').text().trim()).to.equal('(GMT) UTC');
find('#activeTimezone option[value="Africa/Cairo"]').prop('selected', true);
expect(findAll('#activeTimezone option').length, 'available timezones').to.equal(66);
expect(find('#activeTimezone option:checked').textContent.trim()).to.equal('(GMT) UTC');
find('#activeTimezone option[value="Africa/Cairo"]').selected = true;
await triggerEvent('#activeTimezone', 'change');
await click('[data-test-save-button]');
expect(find('#activeTimezone option:selected').text().trim()).to.equal('(GMT +2:00) Cairo, Egypt');
expect(find('#activeTimezone option:checked').textContent.trim()).to.equal('(GMT +2:00) Cairo, Egypt');
});
it('handles private blog settings correctly', async function () {
await visit('/settings/general');
// handles private blog settings correctly
expect(find('[data-test-private-checkbox]').prop('checked'), 'isPrivate checkbox').to.be.false;
expect(find('[data-test-private-checkbox]').checked, 'isPrivate checkbox').to.be.false;
await click('[data-test-private-checkbox]');
expect(find('[data-test-private-checkbox]').prop('checked'), 'isPrivate checkbox').to.be.true;
expect(find('[data-test-password-input]').length, 'password input').to.equal(1);
expect(find('[data-test-password-input]').val(), 'password default value').to.not.equal('');
expect(find('[data-test-private-checkbox]').checked, 'isPrivate checkbox').to.be.true;
expect(findAll('[data-test-password-input]').length, 'password input').to.equal(1);
expect(find('[data-test-password-input]').value, 'password default value').to.not.equal('');
await fillIn('[data-test-password-input]', '');
await triggerEvent('[data-test-password-input]', 'blur');
await blur('[data-test-password-input]');
expect(find('[data-test-password-error]').text().trim(), 'empty password error')
expect(find('[data-test-password-error]').textContent.trim(), 'empty password error')
.to.equal('Password must be supplied');
await fillIn('[data-test-password-input]', 'asdfg');
await triggerEvent('[data-test-password-input]', 'blur');
await blur('[data-test-password-input]');
expect(find('[data-test-password-error]').text().trim(), 'present password error')
expect(find('[data-test-password-error]').textContent.trim(), 'present password error')
.to.equal('');
});
it('handles social blog settings correctly', async function () {
let testSocialInput = async function (type, input, expectedValue, expectedError = '') {
await fillIn(`[data-test-${type}-input]`, input);
await triggerEvent(`[data-test-${type}-input]`, 'blur');
await blur(`[data-test-${type}-input]`);
expect(
find(`[data-test-${type}-input]`).val(),
find(`[data-test-${type}-input]`).value,
`${type} value for ${input}`
).to.equal(expectedValue);
expect(
find(`[data-test-${type}-error]`).text().trim(),
find(`[data-test-${type}-error]`).textContent.trim(),
`${type} validation response for ${input}`
).to.equal(expectedError);
expect(
find(`[data-test-${type}-input]`).closest('.form-group').hasClass('error'),
find(`[data-test-${type}-input]`).closest('.form-group').classList.contains('error'),
`${type} input should be in error state with '${input}'`
).to.equal(!!expectedError);
};
@ -379,15 +374,15 @@ describe('Acceptance: Settings - General', function () {
// validates a facebook url correctly
// loads fixtures and performs transform
expect(find('[data-test-facebook-input]').val(), 'initial facebook value')
expect(find('[data-test-facebook-input]').value, 'initial facebook value')
.to.equal('https://www.facebook.com/test');
await triggerEvent('[data-test-facebook-input]', 'focus');
await triggerEvent('[data-test-facebook-input]', 'blur');
await focus('[data-test-facebook-input]');
await blur('[data-test-facebook-input]');
// regression test: we still have a value after the input is
// focused and then blurred without any changes
expect(find('[data-test-facebook-input]').val(), 'facebook value after blur with no change')
expect(find('[data-test-facebook-input]').value, 'facebook value after blur with no change')
.to.equal('https://www.facebook.com/test');
await testFacebookValidation(
@ -431,15 +426,15 @@ describe('Acceptance: Settings - General', function () {
// validates a twitter url correctly
// loads fixtures and performs transform
expect(find('[data-test-twitter-input]').val(), 'initial twitter value')
expect(find('[data-test-twitter-input]').value, 'initial twitter value')
.to.equal('https://twitter.com/test');
await triggerEvent('[data-test-twitter-input]', 'focus');
await triggerEvent('[data-test-twitter-input]', 'blur');
await focus('[data-test-twitter-input]');
await blur('[data-test-twitter-input]');
// regression test: we still have a value after the input is
// focused and then blurred without any changes
expect(find('[data-test-twitter-input]').val(), 'twitter value after blur with no change')
expect(find('[data-test-twitter-input]').value, 'twitter value after blur with no change')
.to.equal('https://twitter.com/test');
await testTwitterValidation(
@ -469,7 +464,7 @@ describe('Acceptance: Settings - General', function () {
await visit('/settings/general');
expect(
find('[data-test-private-checkbox]').prop('checked'),
find('[data-test-private-checkbox]').checked,
'private blog checkbox'
).to.be.false;
@ -479,16 +474,16 @@ describe('Acceptance: Settings - General', function () {
await click('[data-test-private-checkbox]');
expect(
find('[data-test-private-checkbox]').prop('checked'),
find('[data-test-private-checkbox]').checked,
'private blog checkbox'
).to.be.true;
await visit('/settings/team');
expect(find('.fullscreen-modal').length, 'modal exists').to.equal(1);
expect(findAll('.fullscreen-modal').length, 'modal exists').to.equal(1);
// Leave without saving
await (click('.fullscreen-modal [data-test-leave-button]'), 'leave without saving');
await click('.fullscreen-modal [data-test-leave-button]');
expect(currentURL(), 'currentURL').to.equal('/settings/team');
@ -498,12 +493,12 @@ describe('Acceptance: Settings - General', function () {
// settings were not saved
expect(
find('[data-test-private-checkbox]').prop('checked'),
find('[data-test-private-checkbox]').checked,
'private blog checkbox'
).to.be.false;
expect(
find('[data-test-title-input]').text().trim(),
find('[data-test-title-input]').textContent.trim(),
'Blog title'
).to.equal('');
});

View file

@ -1,99 +1,89 @@
import destroyApp from '../../helpers/destroy-app';
import startApp from '../../helpers/start-app';
import {
afterEach,
beforeEach,
describe,
it
} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {click, currentRouteName, currentURL, fillIn, find, findAll, triggerEvent} from '@ember/test-helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../../helpers/visit';
describe('Acceptance: Settings - Integrations', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
describe('Acceptance: Settings - Integrations - Custom', function () {
let hooks = setupApplicationTest();
setupMirage(hooks);
describe('access permissions', function () {
beforeEach(function () {
server.create('integration', {name: 'Test'});
this.server.create('integration', {name: 'Test'});
});
it('redirects /integrations/ to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/integrations');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('redirects /integrations/ to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects /integrations/ to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects /integrations/ to team page when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/1');
expect(currentURL(), 'currentURL').to.equal('/team');
});
it('redirects /integrations/:id/ to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/integrations/1');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('redirects /integrations/:id/ to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/1');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects /integrations/:id/ to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/1');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects /integrations/:id/ to team page when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/1');
expect(currentURL(), 'currentURL').to.equal('/team');
@ -101,11 +91,11 @@ describe('Acceptance: Settings - Integrations', function () {
});
describe('navigation', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it('renders correctly', async function () {
@ -113,13 +103,13 @@ describe('Acceptance: Settings - Integrations', function () {
// slack is not configured in the fixtures
expect(
find('[data-test-app="slack"] [data-test-app-status]').text().trim(),
find('[data-test-app="slack"] [data-test-app-status]').textContent.trim(),
'slack app status'
).to.equal('Configure');
// amp is enabled in the fixtures
expect(
find('[data-test-app="amp"] [data-test-app-status]').text().trim(),
find('[data-test-app="amp"] [data-test-app-status]').textContent.trim(),
'amp app status'
).to.equal('Active');
});
@ -162,32 +152,32 @@ describe('Acceptance: Settings - Integrations', function () {
});
describe('custom integrations', function () {
beforeEach(function () {
server.loadFixtures('configurations');
let config = server.schema.configurations.first();
beforeEach(async function () {
this.server.loadFixtures('configurations');
let config = this.server.schema.configurations.first();
config.update({
enableDeveloperExperiments: true
});
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it('handles 404', async function () {
await visit('/settings/integrations/1');
expect(currentPath()).to.equal('error404');
expect(currentRouteName()).to.equal('error404');
});
it('can add new integration', async function () {
// sanity check
expect(
server.db.integrations.length,
this.server.db.integrations.length,
'number of integrations in db at start'
).to.equal(0);
expect(
server.db.apiKeys.length,
this.server.db.apiKeys.length,
'number of apiKeys in db at start'
).to.equal(0);
@ -220,7 +210,7 @@ describe('Acceptance: Settings - Integrations', function () {
await click('[data-test-button="create-integration"]');
expect(
find('[data-test-error="new-integration-name"]').text(),
find('[data-test-error="new-integration-name"]').textContent,
'name error after create with blank field'
).to.have.string('enter a name');
@ -228,7 +218,7 @@ describe('Acceptance: Settings - Integrations', function () {
await click('[data-test-button="create-integration"]');
expect(
find('[data-test-error="new-integration-name"]').text(),
find('[data-test-error="new-integration-name"]').textContent,
'name error after create with duplicate name'
).to.have.string('already been used');
@ -236,7 +226,7 @@ describe('Acceptance: Settings - Integrations', function () {
await fillIn('[data-test-input="new-integration-name"]', 'Test');
expect(
find('[data-test-error="new-integration-name"]').text().trim(),
find('[data-test-error="new-integration-name"]').textContent.trim(),
'name error after typing in field'
).to.be.empty;
@ -248,12 +238,12 @@ describe('Acceptance: Settings - Integrations', function () {
).to.not.exist;
expect(
server.db.integrations.length,
this.server.db.integrations.length,
'number of integrations in db after create'
).to.equal(1);
// mirage sanity check
expect(
server.db.apiKeys.length,
this.server.db.apiKeys.length,
'number of api keys in db after create'
).to.equal(2);
@ -276,7 +266,7 @@ describe('Acceptance: Settings - Integrations', function () {
).to.not.exist;
expect(
find('[data-test-custom-integration]').length,
findAll('[data-test-custom-integration]').length,
'number of custom integrations after creation'
).to.equal(1);
@ -289,7 +279,7 @@ describe('Acceptance: Settings - Integrations', function () {
});
it('can manage an integration', async function () {
server.create('integration');
this.server.create('integration');
await visit('/settings/integrations/1');
@ -299,7 +289,7 @@ describe('Acceptance: Settings - Integrations', function () {
).to.equal('/settings/integrations/1');
expect(
find('[data-test-screen-title]').text(),
find('[data-test-screen-title]').textContent,
'screen title'
).to.have.string('Integration 1');
@ -307,29 +297,29 @@ describe('Acceptance: Settings - Integrations', function () {
// TODO: add test for logo
expect(
find('[data-test-input="name"]').val(),
find('[data-test-input="name"]').value,
'initial name value'
).to.equal('Integration 1');
expect(
find('[data-test-input="description"]').val(),
find('[data-test-input="description"]').value,
'initial description value'
).to.equal('');
expect(
find('[data-test-input="content_key"]').val(),
find('[data-test-input="content_key"]').value,
'content key input value'
).to.equal('integration-1_content_key-12345');
expect(
find('[data-test-input="admin_key"]').val(),
find('[data-test-input="admin_key"]').value,
'admin key input value'
).to.equal('integration-1_admin_key-12345');
// it can modify integration fields and has validation
expect(
find('[data-test-error="name"]').text().trim(),
find('[data-test-error="name"]').textContent.trim(),
'initial name error'
).to.be.empty;
@ -337,14 +327,14 @@ describe('Acceptance: Settings - Integrations', function () {
await triggerEvent('[data-test-input="name"]', 'blur');
expect(
find('[data-test-error="name"]').text(),
find('[data-test-error="name"]').textContent,
'name validation for blank string'
).to.have.string('enter a name');
await click('[data-test-button="save"]');
expect(
server.schema.integrations.first().name,
this.server.schema.integrations.first().name,
'db integration name after failed save'
).to.equal('Integration 1');
@ -352,7 +342,7 @@ describe('Acceptance: Settings - Integrations', function () {
await triggerEvent('[data-test-input="name"]', 'blur');
expect(
find('[data-test-error="name"]').text().trim(),
find('[data-test-error="name"]').textContent.trim(),
'name error after valid entry'
).to.be.empty;
@ -370,12 +360,12 @@ describe('Acceptance: Settings - Integrations', function () {
).to.equal('/settings/integrations');
expect(
find('[data-test-integration="1"] [data-test-text="name"]').text().trim(),
find('[data-test-integration="1"] [data-test-text="name"]').textContent.trim(),
'integration name after save'
).to.equal('Test Integration');
expect(
find('[data-test-integration="1"] [data-test-text="description"]').text().trim(),
find('[data-test-integration="1"] [data-test-text="description"]').textContent.trim(),
'integration description after save'
).to.equal('Description for Test Integration');
@ -387,14 +377,14 @@ describe('Acceptance: Settings - Integrations', function () {
await click('[data-test-link="integrations-back"]');
expect(
find('[data-modal="unsaved-settings"]'),
find('[data-test-modal="unsaved-settings"]'),
'modal shown when navigating with unsaved changes'
).to.exist;
await click('[data-test-stay-button]');
expect(
find('[data-modal="unsaved-settings"]'),
find('[data-test-modal="unsaved-settings"]'),
'modal is closed after clicking "stay"'
).to.not.exist;
@ -407,7 +397,7 @@ describe('Acceptance: Settings - Integrations', function () {
await click('[data-test-leave-button]');
expect(
find('[data-modal="unsaved-settings"]'),
find('[data-test-modal="unsaved-settings"]'),
'modal is closed after clicking "leave"'
).to.not.exist;
@ -417,27 +407,27 @@ describe('Acceptance: Settings - Integrations', function () {
).to.equal('/settings/integrations');
expect(
find('[data-test-integration="1"] [data-test-text="name"]').text().trim(),
find('[data-test-integration="1"] [data-test-text="name"]').textContent.trim(),
'integration name after leaving unsaved changes'
).to.equal('Test Integration');
});
it('can manage an integration\'s webhooks', async function () {
server.create('integration');
this.server.create('integration');
await visit('/settings/integrations/1');
expect(find('[data-test-webhooks-blank-slate]').length).to.equal(1);
expect(find('[data-test-webhooks-blank-slate]')).to.exist;
// open new webhook modal
await click('[data-test-link="add-webhook"]');
expect(find('[data-test-modal="webhook-form"]').length).to.equal(1);
expect(find('[data-test-modal="webhook-form"] [data-test-text="title"]').text())
expect(find('[data-test-modal="webhook-form"]')).to.exist;
expect(find('[data-test-modal="webhook-form"] [data-test-text="title"]').textContent)
.to.have.string('New webhook');
// can cancel new webhook
await click('[data-test-button="cancel-webhook"]');
expect(find('[data-test-modal="webhook-form"]').length).to.equal(0);
expect(find('[data-test-modal="webhook-form"]')).to.not.exist;
// create new webhook
await click('[data-test-link="add-webhook"]');
@ -447,30 +437,30 @@ describe('Acceptance: Settings - Integrations', function () {
await click('[data-test-button="save-webhook"]');
// modal closed and 1 webhook listed with correct details
expect(find('[data-test-modal="webhook-form"]').length).to.equal(0);
expect(find('[data-test-webhook-row]').length).to.equal(1);
expect(find('[data-test-modal="webhook-form"]')).to.not.exist;
expect(find('[data-test-webhook-row]')).to.exist;
let row = find('[data-test-webhook-row="1"]');
expect(row.find('[data-test-text="name"]').text())
expect(row.querySelector('[data-test-text="name"]').textContent)
.to.have.string('First webhook');
expect(row.find('[data-test-text="event"]').text())
expect(row.querySelector('[data-test-text="event"]').textContent)
.to.have.string('Site Changed (rebuild)');
expect(row.find('[data-test-text="targetUrl"]').text())
expect(row.querySelector('[data-test-text="targetUrl"]').textContent)
.to.have.string('https://example.com/first-webhook');
expect(row.find('[data-test-text="last-triggered"]').text())
expect(row.querySelector('[data-test-text="last-triggered"]').textContent)
.to.have.string('Not triggered');
// click edit webhook link
await click('[data-test-webhook-row="1"] [data-test-link="edit-webhook"]');
// modal appears and has correct title
expect(find('[data-test-modal="webhook-form"]').length).to.equal(1);
expect(find('[data-test-modal="webhook-form"] [data-test-text="title"]').text())
expect(find('[data-test-modal="webhook-form"]')).to.exist;
expect(find('[data-test-modal="webhook-form"] [data-test-text="title"]').textContent)
.to.have.string('Edit webhook');
});
// test to ensure the `value=description` passed to `gh-text-input` is `readonly`
it('doesn\'t show unsaved changes modal after placing focus on description field', async function () {
server.create('integration');
this.server.create('integration');
await visit('/settings/integrations/1');
await click('[data-test-input="description"]');
@ -478,7 +468,7 @@ describe('Acceptance: Settings - Integrations', function () {
await click('[data-test-link="integrations-back"]');
expect(
find('[data-modal="unsaved-settings"]'),
find('[data-test-modal="unsaved-settings"]'),
'unsaved changes modal is not shown'
).to.not.exist;

View file

@ -1,66 +1,61 @@
import $ from 'jquery';
import destroyApp from '../../helpers/destroy-app';
import startApp from '../../helpers/start-app';
// import wait from 'ember-test-helpers/wait';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {click, currentURL, find, findAll} from '@ember/test-helpers';
import {expect} from 'chai';
import {fileUpload} from '../../helpers/file-upload';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../../helpers/visit';
// import wait from 'ember-test-helpers/wait';
// import {timeout} from 'ember-concurrency';
describe('Acceptance: Settings - Labs', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/labs');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('redirects to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/labs');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/labs');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/labs');
expect(currentURL(), 'currentURL').to.equal('/team');
});
describe('when logged in', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it.skip('it renders, loads modals correctly', async function () {
@ -73,24 +68,24 @@ describe('Acceptance: Settings - Labs', function () {
expect(document.title, 'page title').to.equal('Settings - Labs - Test Blog');
// highlights nav menu
expect($('[data-test-nav="labs"]').hasClass('active'), 'highlights nav menu item')
.to.be.true;
expect(find('[data-test-nav="labs"]'), 'highlights nav menu item')
.to.have.class('active');
await click('#settings-resetdb .js-delete');
expect(find('.fullscreen-modal .modal-content').length, 'modal element').to.equal(1);
expect(findAll('.fullscreen-modal .modal-content').length, 'modal element').to.equal(1);
await click('.fullscreen-modal .modal-footer .gh-btn');
expect(find('.fullscreen-modal').length, 'modal element').to.equal(0);
expect(findAll('.fullscreen-modal').length, 'modal element').to.equal(0);
});
it('can upload/download redirects', async function () {
await visit('/settings/labs');
// successful upload
server.post('/redirects/json/', {}, 200);
this.server.post('/redirects/json/', {}, 200);
await fileUpload(
'[data-test-file-input="redirects"]',
'[data-test-file-input="redirects"] input',
['test'],
{name: 'redirects.json', type: 'application/json'}
);
@ -102,33 +97,33 @@ describe('Acceptance: Settings - Labs', function () {
// await timeout(50);
//
// // shows success button
// let button = find('[data-test-button="upload-redirects"]');
// expect(button.length, 'no of success buttons').to.equal(1);
// let buttons = findAll('[data-test-button="upload-redirects"]');
// expect(buttons.length, 'no of success buttons').to.equal(1);
// expect(
// button.hasClass('gh-btn-green'),
// buttons[0],
// 'success button is green'
// ).to.be.true;
// ).to.have.class('gh-btn-green);
// expect(
// button.text().trim(),
// button.textContent,
// 'success button text'
// ).to.have.string('Uploaded');
//
// await wait();
// returned to normal button
let button = find('[data-test-button="upload-redirects"]');
expect(button.length, 'no of post-success buttons').to.equal(1);
let buttons = findAll('[data-test-button="upload-redirects"]');
expect(buttons.length, 'no of post-success buttons').to.equal(1);
expect(
button.hasClass('gh-btn-green'),
buttons[0],
'post-success button doesn\'t have success class'
).to.be.false;
).to.not.have.class('gh-btn-green');
expect(
button.text().trim(),
buttons[0].textContent,
'post-success button text'
).to.have.string('Upload redirects');
// failed upload
server.post('/redirects/json/', {
this.server.post('/redirects/json/', {
errors: [{
errorType: 'BadRequestError',
message: 'Test failure message'
@ -136,7 +131,7 @@ describe('Acceptance: Settings - Labs', function () {
}, 400);
await fileUpload(
'[data-test-file-input="redirects"]',
'[data-test-file-input="redirects"] input',
['test'],
{name: 'redirects-bad.json', type: 'application/json'}
);
@ -148,14 +143,14 @@ describe('Acceptance: Settings - Labs', function () {
// await timeout(50);
//
// shows failure button
// button = find('[data-test-button="upload-redirects"]');
// expect(button.length, 'no of failure buttons').to.equal(1);
// buttons = findAll('[data-test-button="upload-redirects"]');
// expect(buttons.length, 'no of failure buttons').to.equal(1);
// expect(
// button.hasClass('gh-btn-red'),
// buttons[0],
// 'failure button is red'
// ).to.be.true;
// ).to.have.class('gh-btn-red);
// expect(
// button.text().trim(),
// buttons[0].textContent,
// 'failure button text'
// ).to.have.string('Upload Failed');
//
@ -163,26 +158,26 @@ describe('Acceptance: Settings - Labs', function () {
// shows error message
expect(
find('[data-test-error="redirects"]').text().trim(),
find('[data-test-error="redirects"]').textContent.trim(),
'upload error text'
).to.have.string('Test failure message');
// returned to normal button
button = find('[data-test-button="upload-redirects"]');
expect(button.length, 'no of post-failure buttons').to.equal(1);
buttons = findAll('[data-test-button="upload-redirects"]');
expect(buttons.length, 'no of post-failure buttons').to.equal(1);
expect(
button.hasClass('gh-btn-red'),
buttons[0],
'post-failure button doesn\'t have failure class'
).to.be.false;
).to.not.have.class('gh-btn-red');
expect(
button.text().trim(),
buttons[0].textContent,
'post-failure button text'
).to.have.string('Upload redirects');
// successful upload clears error
server.post('/redirects/json/', {}, 200);
this.server.post('/redirects/json/', {}, 200);
await fileUpload(
'[data-test-file-input="redirects"]',
'[data-test-file-input="redirects"] input',
['test'],
{name: 'redirects-bad.json', type: 'application/json'}
);
@ -192,18 +187,18 @@ describe('Acceptance: Settings - Labs', function () {
// can download redirects.json
await click('[data-test-link="download-redirects"]');
let iframe = $('#iframeDownload');
expect(iframe.attr('src')).to.have.string('/redirects/json/');
let iframe = document.querySelector('#iframeDownload');
expect(iframe.getAttribute('src')).to.have.string('/redirects/json/');
});
it('can upload/download routes.yaml', async function () {
await visit('/settings/labs');
// successful upload
server.post('/settings/routes/yaml/', {}, 200);
this.server.post('/settings/routes/yaml/', {}, 200);
await fileUpload(
'[data-test-file-input="routes"]',
'[data-test-file-input="routes"] input',
['test'],
{name: 'routes.yaml', type: 'application/x-yaml'}
);
@ -229,19 +224,19 @@ describe('Acceptance: Settings - Labs', function () {
// await wait();
// returned to normal button
let button = find('[data-test-button="upload-routes"]');
expect(button.length, 'no of post-success buttons').to.equal(1);
let buttons = findAll('[data-test-button="upload-routes"]');
expect(buttons.length, 'no of post-success buttons').to.equal(1);
expect(
button.hasClass('gh-btn-green'),
buttons[0],
'routes post-success button doesn\'t have success class'
).to.be.false;
).to.not.have.class('gh-btn-green');
expect(
button.text().trim(),
buttons[0].textContent,
'routes post-success button text'
).to.have.string('Upload routes YAML');
// failed upload
server.post('/settings/routes/yaml/', {
this.server.post('/settings/routes/yaml/', {
errors: [{
errorType: 'BadRequestError',
message: 'Test failure message'
@ -249,7 +244,7 @@ describe('Acceptance: Settings - Labs', function () {
}, 400);
await fileUpload(
'[data-test-file-input="routes"]',
'[data-test-file-input="routes"] input',
['test'],
{name: 'routes-bad.yaml', type: 'application/x-yaml'}
);
@ -276,26 +271,26 @@ describe('Acceptance: Settings - Labs', function () {
// shows error message
expect(
find('[data-test-error="routes"]').text().trim(),
find('[data-test-error="routes"]').textContent,
'routes upload error text'
).to.have.string('Test failure message');
// returned to normal button
button = find('[data-test-button="upload-routes"]');
expect(button.length, 'no of post-failure buttons').to.equal(1);
buttons = findAll('[data-test-button="upload-routes"]');
expect(buttons.length, 'no of post-failure buttons').to.equal(1);
expect(
button.hasClass('gh-btn-red'),
buttons[0],
'routes post-failure button doesn\'t have failure class'
).to.be.false;
).to.not.have.class('gh-btn-red');
expect(
button.text().trim(),
buttons[0].textContent,
'routes post-failure button text'
).to.have.string('Upload routes YAML');
// successful upload clears error
server.post('/settings/routes/yaml/', {}, 200);
this.server.post('/settings/routes/yaml/', {}, 200);
await fileUpload(
'[data-test-file-input="routes"]',
'[data-test-file-input="routes"] input',
['test'],
{name: 'routes-good.yaml', type: 'application/x-yaml'}
);
@ -305,8 +300,8 @@ describe('Acceptance: Settings - Labs', function () {
// can download redirects.json
await click('[data-test-link="download-routes"]');
let iframe = $('#iframeDownload');
expect(iframe.attr('src')).to.have.string('/settings/routes/yaml/');
let iframe = document.querySelector('#iframeDownload');
expect(iframe.getAttribute('src')).to.have.string('/settings/routes/yaml/');
});
});
});

View file

@ -1,65 +1,60 @@
import Mirage from 'ember-cli-mirage';
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import destroyApp from '../../helpers/destroy-app';
import startApp from '../../helpers/start-app';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {blur, click, currentURL, fillIn, find, findAll, triggerEvent} from '@ember/test-helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../../helpers/visit';
describe('Acceptance: Settings - Integrations - Slack', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/integrations/slack');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('redirects to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/slack');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/slack');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/slack');
expect(currentURL(), 'currentURL').to.equal('/team');
});
describe('when logged in', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it('it validates and saves a slack url properly', async function () {
@ -71,7 +66,7 @@ describe('Acceptance: Settings - Integrations - Slack', function () {
await fillIn('[data-test-slack-url-input]', 'notacorrecturl');
await click('[data-test-save-button]');
expect(find('#slack-settings .error .response').text().trim(), 'inline validation response')
expect(find('[data-test-error="slack-url"').textContent.trim(), 'inline validation response')
.to.equal('The URL must be in a format like https://hooks.slack.com/services/<your personal key>');
// CMD-S shortcut works
@ -82,22 +77,22 @@ describe('Acceptance: Settings - Integrations - Slack', function () {
ctrlKey: ctrlOrCmd === 'ctrl'
});
let [newRequest] = server.pretender.handledRequests.slice(-1);
let [newRequest] = this.server.pretender.handledRequests.slice(-1);
let params = JSON.parse(newRequest.requestBody);
let [result] = JSON.parse(params.settings.findBy('key', 'slack').value);
expect(result.url).to.equal('https://hooks.slack.com/services/1275958430');
expect(find('#slack-settings .error .response').text().trim(), 'inline validation response')
.to.equal('');
expect(find('[data-test-error="slack-url"'), 'inline validation response')
.to.not.exist;
await fillIn('[data-test-slack-url-input]', 'https://hooks.slack.com/services/1275958430');
await click('[data-test-send-notification-button]');
expect(find('.gh-notification').length, 'number of notifications').to.equal(1);
expect(find('#slack-settings .error .response').text().trim(), 'inline validation response')
.to.equal('');
expect(findAll('.gh-notification').length, 'number of notifications').to.equal(1);
expect(find('[data-test-error="slack-url"'), 'inline validation response')
.to.not.exist;
server.put('/settings/', function () {
this.server.put('/settings/', function () {
return new Mirage.Response(422, {}, {
errors: [
{
@ -112,9 +107,9 @@ describe('Acceptance: Settings - Integrations - Slack', function () {
await click('[data-test-send-notification-button]');
// we shouldn't try to send the test request if the save fails
let [lastRequest] = server.pretender.handledRequests.slice(-1);
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.url).to.not.match(/\/slack\/test/);
expect(find('.gh-notification').length, 'check slack notification after api validation error').to.equal(0);
expect(findAll('.gh-notification').length, 'check slack notification after api validation error').to.equal(0);
});
it('warns when leaving without saving', async function () {
@ -124,14 +119,14 @@ describe('Acceptance: Settings - Integrations - Slack', function () {
expect(currentURL(), 'currentURL').to.equal('/settings/integrations/slack');
await fillIn('[data-test-slack-url-input]', 'https://hooks.slack.com/services/1275958430');
await triggerEvent('[data-test-slack-url-input]', 'blur');
await blur('[data-test-slack-url-input]');
await visit('/settings/design');
expect(find('.fullscreen-modal').length, 'modal exists').to.equal(1);
expect(findAll('.fullscreen-modal').length, 'modal exists').to.equal(1);
// Leave without saving
await (click('.fullscreen-modal [data-test-leave-button]'), 'leave without saving');
await click('.fullscreen-modal [data-test-leave-button]');
expect(currentURL(), 'currentURL').to.equal('/settings/design');
@ -141,7 +136,7 @@ describe('Acceptance: Settings - Integrations - Slack', function () {
// settings were not saved
expect(
find('[data-test-slack-url-input]').text().trim(),
find('[data-test-slack-url-input]').textContent.trim(),
'Slack Webhook URL'
).to.equal('');
});

View file

@ -1,16 +1,16 @@
/* eslint-disable camelcase */
import $ from 'jquery';
import destroyApp from '../../helpers/destroy-app';
import startApp from '../../helpers/start-app';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import wait from 'ember-test-helpers/wait';
import windowProxy from 'ghost-admin/utils/window-proxy';
import {Response} from 'ember-cli-mirage';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {blur, click, currentRouteName, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
import {errorOverride, errorReset} from 'ghost-admin/tests/helpers/adapter-error';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupApplicationTest} from 'ember-mocha';
import {timeout} from 'ember-concurrency';
import {visit} from '../../helpers/visit';
// Grabbed from keymaster's testing code because Ember's `keyEvent` helper
// is for some reason not triggering the events in a way that keymaster detects:
@ -40,38 +40,31 @@ let keyup = function (code, el) {
};
describe('Acceptance: Settings - Tags', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/tags');
expect(currentURL()).to.equal('/signin');
});
it('redirects to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/design');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/design');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
@ -80,9 +73,9 @@ describe('Acceptance: Settings - Tags', function () {
describe('when logged in', function () {
let newLocation, originalReplaceState;
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
originalReplaceState = windowProxy.replaceState;
windowProxy.replaceState = function (params, title, url) {
@ -90,7 +83,7 @@ describe('Acceptance: Settings - Tags', function () {
};
newLocation = undefined;
return authenticateSession(application);
return await authenticateSession();
});
afterEach(function () {
@ -98,8 +91,8 @@ describe('Acceptance: Settings - Tags', function () {
});
it('it renders, can be navigated, can edit, create & delete tags', async function () {
let tag1 = server.create('tag');
let tag2 = server.create('tag');
let tag1 = this.server.create('tag');
let tag2 = this.server.create('tag');
await visit('/settings/tags');
@ -113,37 +106,39 @@ describe('Acceptance: Settings - Tags', function () {
expect(document.title, 'page title').to.equal('Settings - Tags - Test Blog');
// it highlights nav menu
expect($('[data-test-nav="tags"]').hasClass('active'), 'highlights nav menu item')
.to.be.true;
expect(find('[data-test-nav="tags"]'), 'highlights nav menu item')
.to.have.class('active');
// it lists all tags
expect(find('.settings-tags .settings-tag').length, 'tag list count')
expect(findAll('.settings-tags .settings-tag').length, 'tag list count')
.to.equal(2);
expect(find('.settings-tags .settings-tag:first .tag-title').text(), 'tag list item title')
let tag = find('.settings-tags .settings-tag');
expect(tag.querySelector('.tag-title').textContent, 'tag list item title')
.to.equal(tag1.name);
// it highlights selected tag
expect(find(`a[href="/ghost/settings/tags/${tag1.slug}"]`).hasClass('active'), 'highlights selected tag')
.to.be.true;
expect(find(`a[href="/ghost/settings/tags/${tag1.slug}"]`), 'highlights selected tag')
.to.have.class('active');
// it shows selected tag form
expect(find('.tag-settings-pane h4').text(), 'settings pane title')
expect(find('.tag-settings-pane h4').textContent, 'settings pane title')
.to.equal('Tag Settings');
expect(find('.tag-settings-pane input[name="name"]').val(), 'loads correct tag into form')
expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form')
.to.equal(tag1.name);
// click the second tag in the list
await click('.tag-edit-button:last');
let tagEditButtons = findAll('.tag-edit-button');
await click(tagEditButtons[tagEditButtons.length - 1]);
// it navigates to selected tag
expect(currentURL(), 'url after clicking tag').to.equal(`/settings/tags/${tag2.slug}`);
// it highlights selected tag
expect(find(`a[href="/ghost/settings/tags/${tag2.slug}"]`).hasClass('active'), 'highlights selected tag')
.to.be.true;
expect(find(`a[href="/ghost/settings/tags/${tag2.slug}"]`), 'highlights selected tag')
.to.have.class('active');
// it shows selected tag form
expect(find('.tag-settings-pane input[name="name"]').val(), 'loads correct tag into form')
expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form')
.to.equal(tag2.name);
// simulate up arrow press
@ -158,8 +153,8 @@ describe('Acceptance: Settings - Tags', function () {
expect(currentURL(), 'url after keyboard up arrow').to.equal(`/settings/tags/${tag1.slug}`);
// it highlights selected tag
expect(find(`a[href="/ghost/settings/tags/${tag1.slug}"]`).hasClass('active'), 'selects previous tag')
.to.be.true;
expect(find(`a[href="/ghost/settings/tags/${tag1.slug}"]`), 'selects previous tag')
.to.have.class('active');
// simulate down arrow press
run(() => {
@ -173,21 +168,23 @@ describe('Acceptance: Settings - Tags', function () {
expect(currentURL(), 'url after keyboard down arrow').to.equal(`/settings/tags/${tag2.slug}`);
// it highlights selected tag
expect(find(`a[href="/ghost/settings/tags/${tag2.slug}"]`).hasClass('active'), 'selects next tag')
.to.be.true;
expect(find(`a[href="/ghost/settings/tags/${tag2.slug}"]`), 'selects next tag')
.to.have.class('active');
// trigger save
await fillIn('.tag-settings-pane input[name="name"]', 'New Name');
await triggerEvent('.tag-settings-pane input[name="name"]', 'blur');
await blur('.tag-settings-pane input[name="name"]');
// extra timeout needed for Travis - sometimes it doesn't update
// quick enough and an extra wait() call doesn't help
await timeout(100);
// check we update with the data returned from the server
expect(find('.settings-tag')[0].querySelector('.tag-title').textContent.trim(), 'tag list updates on save')
let tags = findAll('.settings-tags .settings-tag');
tag = tags[0];
expect(tag.querySelector('.tag-title').textContent, 'tag list updates on save')
.to.equal('New Name');
expect(find('.tag-settings-pane input[name="name"]').val(), 'settings form updates on save')
expect(find('.tag-settings-pane input[name="name"]').value, 'settings form updates on save')
.to.equal('New Name');
// start new tag
@ -197,18 +194,18 @@ describe('Acceptance: Settings - Tags', function () {
expect(currentURL(), 'new tag URL').to.equal('/settings/tags/new');
// it displays the new tag form
expect(find('.tag-settings-pane h4').text(), 'settings pane title')
expect(find('.tag-settings-pane h4').textContent, 'settings pane title')
.to.equal('New Tag');
// all fields start blank
find('.tag-settings-pane input, .tag-settings-pane textarea').each(function () {
expect($(this).val(), `input field for ${$(this).attr('name')}`)
findAll('.tag-settings-pane input, .tag-settings-pane textarea').forEach(function (elem) {
expect(elem.value, `input field for ${elem.getAttribute('name')}`)
.to.be.empty;
});
// save new tag
await fillIn('.tag-settings-pane input[name="name"]', 'New Tag');
await triggerEvent('.tag-settings-pane input[name="name"]', 'blur');
await blur('.tag-settings-pane input[name="name"]');
// extra timeout needed for FF on Linux - sometimes it doesn't update
// quick enough, especially on Travis, and an extra wait() call
@ -219,13 +216,17 @@ describe('Acceptance: Settings - Tags', function () {
expect(currentURL(), 'URL after tag creation').to.equal('/settings/tags/new-tag');
// it adds the tag to the list and selects
expect(find('.settings-tags .settings-tag').length, 'tag list count after creation')
tags = findAll('.settings-tags .settings-tag');
tag = tags[1]; // second tag in list due to alphabetical ordering
expect(tags.length, 'tag list count after creation')
.to.equal(3);
// new tag will be second in the list due to alphabetical sorting
expect(find('.settings-tags .settings-tag')[1].querySelector('.tag-title').textContent.trim(), 'new tag list item title')
expect(findAll('.settings-tags .settings-tag')[1].querySelector('.tag-title').textContent.trim(), 'new tag list item title');
expect(tag.querySelector('.tag-title').textContent, 'new tag list item title')
.to.equal('New Tag');
expect(find('a[href="/ghost/settings/tags/new-tag"]').hasClass('active'), 'highlights new tag')
.to.be.true;
expect(find('a[href="/ghost/settings/tags/new-tag"]'), 'highlights new tag')
.to.have.class('active');
// delete tag
await click('.settings-menu-delete-button');
@ -235,7 +236,7 @@ describe('Acceptance: Settings - Tags', function () {
expect(currentURL(), 'URL after tag deletion').to.equal(`/settings/tags/${tag1.slug}`);
// it removes the tag from the list
expect(find('.settings-tags .settings-tag').length, 'tag list count after deletion')
expect(findAll('.settings-tags .settings-tag').length, 'tag list count after deletion')
.to.equal(2);
});
@ -243,7 +244,7 @@ describe('Acceptance: Settings - Tags', function () {
// skipped because it was failing most of the time on Travis
// see https://github.com/TryGhost/Ghost/issues/8805
it.skip('loads tag via slug when accessed directly', async function () {
server.createList('tag', 2);
this.server.createList('tag', 2);
await visit('/settings/tags/tag-1');
@ -253,20 +254,20 @@ describe('Acceptance: Settings - Tags', function () {
expect(currentURL(), 'URL after direct load').to.equal('/settings/tags/tag-1');
// it loads all other tags
expect(find('.settings-tags .settings-tag').length, 'tag list count after direct load')
expect(findAll('.settings-tags .settings-tag').length, 'tag list count after direct load')
.to.equal(2);
// selects tag in list
expect(find('a[href="/ghost/settings/tags/tag-1"]').hasClass('active'), 'highlights requested tag')
expect(find('a[href="/ghost/settings/tags/tag-1"]').classList.contains('active'), 'highlights requested tag')
.to.be.true;
// shows requested tag in settings pane
expect(find('.tag-settings-pane input[name="name"]').val(), 'loads correct tag into form')
expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form')
.to.equal('Tag 1');
});
it('shows the internal tag label', async function () {
server.create('tag', {name: '#internal-tag', slug: 'hash-internal-tag', visibility: 'internal'});
this.server.create('tag', {name: '#internal-tag', slug: 'hash-internal-tag', visibility: 'internal'});
await visit('settings/tags/');
@ -275,18 +276,20 @@ describe('Acceptance: Settings - Tags', function () {
expect(currentURL()).to.equal('/settings/tags/hash-internal-tag');
expect(find('.settings-tags .settings-tag').length, 'tag list count')
expect(findAll('.settings-tags .settings-tag').length, 'tag list count')
.to.equal(1);
expect(find('.settings-tags .settings-tag:first .label.label-blue').length, 'internal tag label')
let tag = find('.settings-tags .settings-tag');
expect(tag.querySelectorAll('.label.label-blue').length, 'internal tag label')
.to.equal(1);
expect(find('.settings-tags .settings-tag:first .label.label-blue').text().trim(), 'internal tag label text')
expect(tag.querySelector('.label.label-blue').textContent.trim(), 'internal tag label text')
.to.equal('internal');
});
it('updates the URL when slug changes', async function () {
server.createList('tag', 2);
this.server.createList('tag', 2);
await visit('/settings/tags/tag-1');
@ -297,7 +300,7 @@ describe('Acceptance: Settings - Tags', function () {
// update the slug
await fillIn('.tag-settings-pane input[name="slug"]', 'test');
await triggerEvent('.tag-settings-pane input[name="slug"]', 'blur');
await blur('.tag-settings-pane input[name="slug"]');
// tests don't have a location.hash so we can only check that the
// slug portion is updated correctly
@ -305,7 +308,7 @@ describe('Acceptance: Settings - Tags', function () {
});
it('redirects to 404 when tag does not exist', async function () {
server.get('/tags/slug/unknown/', function () {
this.server.get('/tags/slug/unknown/', function () {
return new Response(404, {'Content-Type': 'application/json'}, {errors: [{message: 'Tag not found.', errorType: 'NotFoundError'}]});
});
@ -314,25 +317,27 @@ describe('Acceptance: Settings - Tags', function () {
await visit('settings/tags/unknown');
errorReset();
expect(currentPath()).to.equal('error404');
expect(currentRouteName()).to.equal('error404');
expect(currentURL()).to.equal('/settings/tags/unknown');
});
it('sorts tags correctly', async function () {
server.create('tag', {name: 'B - Second', slug: 'second'});
server.create('tag', {name: 'Z - Last', slug: 'last'});
server.create('tag', {name: 'A - First', slug: 'first'});
this.server.create('tag', {name: 'B - Third', slug: 'third'});
this.server.create('tag', {name: 'Z - Last', slug: 'last'});
this.server.create('tag', {name: '#A - Second', slug: 'second'});
this.server.create('tag', {name: 'A - First', slug: 'first'});
await visit('settings/tags');
// second wait is needed for the vertical-collection to settle
await wait();
let tags = find('[data-test-tag]');
let tags = findAll('[data-test-tag]');
expect(tags[0].querySelector('[data-test-name]').textContent.trim()).to.equal('A - First');
expect(tags[1].querySelector('[data-test-name]').textContent.trim()).to.equal('B - Second');
expect(tags[2].querySelector('[data-test-name]').textContent.trim()).to.equal('Z - Last');
expect(tags[1].querySelector('[data-test-name]').textContent.trim()).to.equal('#A - Second');
expect(tags[2].querySelector('[data-test-name]').textContent.trim()).to.equal('B - Third');
expect(tags[3].querySelector('[data-test-name]').textContent.trim()).to.equal('Z - Last');
});
});
});

View file

@ -1,64 +1,63 @@
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import destroyApp from '../../helpers/destroy-app';
import startApp from '../../helpers/start-app';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {
beforeEach,
describe,
it
} from 'mocha';
import {click, currentURL, find, findAll, triggerEvent} from '@ember/test-helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../../helpers/visit';
describe('Acceptance: Settings - Integrations - Unsplash', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/integrations/unsplash');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('redirects to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/unsplash');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/unsplash');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/unsplash');
expect(currentURL(), 'currentURL').to.equal('/team');
});
describe('when logged in', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it('it can activate/deactivate', async function () {
@ -69,13 +68,13 @@ describe('Acceptance: Settings - Integrations - Unsplash', function () {
// verify we don't have an unsplash setting fixture loaded
expect(
server.db.settings.where({key: 'unsplash'}),
this.server.db.settings.where({key: 'unsplash'}),
'initial server settings'
).to.be.empty;
// it's enabled by default when settings is empty
expect(
find('[data-test-checkbox="unsplash"]').prop('checked'),
find('[data-test-checkbox="unsplash"]').checked,
'checked by default'
).to.be.true;
@ -83,12 +82,12 @@ describe('Acceptance: Settings - Integrations - Unsplash', function () {
await click('[data-test-save-button]');
// server should now have an unsplash setting
let [setting] = server.db.settings.where({key: 'unsplash'});
let [setting] = this.server.db.settings.where({key: 'unsplash'});
expect(setting, 'unsplash setting after save').to.exist;
expect(setting.value).to.equal('{"isActive":true}');
// disable
await click(find('[data-test-checkbox="unsplash"]'));
await click('[data-test-checkbox="unsplash"]');
// save via CMD-S shortcut
await triggerEvent('.gh-app', 'keydown', {
@ -98,7 +97,7 @@ describe('Acceptance: Settings - Integrations - Unsplash', function () {
});
// server should have an updated setting
[setting] = server.db.settings.where({key: 'unsplash'});
[setting] = this.server.db.settings.where({key: 'unsplash'});
expect(setting.value).to.equal('{"isActive":false}');
});
@ -109,20 +108,20 @@ describe('Acceptance: Settings - Integrations - Unsplash', function () {
expect(currentURL(), 'currentURL').to.equal('/settings/integrations/unsplash');
expect(
find('[data-test-checkbox="unsplash"]').prop('checked'),
find('[data-test-checkbox="unsplash"]').checked,
'checked by default'
).to.be.true;
await click('[data-test-checkbox="unsplash"]');
expect(find('[data-test-checkbox="unsplash"]').prop('checked'), 'Unsplash checkbox').to.be.false;
expect(find('[data-test-checkbox="unsplash"]').checked, 'Unsplash checkbox').to.be.false;
await visit('/settings/labs');
expect(find('.fullscreen-modal').length, 'modal exists').to.equal(1);
expect(findAll('.fullscreen-modal').length, 'modal exists').to.equal(1);
// Leave without saving
await (click('.fullscreen-modal [data-test-leave-button]'), 'leave without saving');
await click('.fullscreen-modal [data-test-leave-button]');
expect(currentURL(), 'currentURL').to.equal('/settings/labs');
@ -131,7 +130,7 @@ describe('Acceptance: Settings - Integrations - Unsplash', function () {
expect(currentURL(), 'currentURL').to.equal('/settings/integrations/unsplash');
// settings were not saved
expect(find('[data-test-checkbox="unsplash"]').prop('checked'), 'Unsplash checkbox').to.be.true;
expect(find('[data-test-checkbox="unsplash"]').checked, 'Unsplash checkbox').to.be.true;
});
});
});

View file

@ -1,68 +1,62 @@
import destroyApp from '../../helpers/destroy-app';
import startApp from '../../helpers/start-app';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {
afterEach,
beforeEach,
describe,
it
} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {currentURL} from '@ember/test-helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../../helpers/visit';
describe('Acceptance: Settings - Integrations - Zapier', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/settings/integrations/zapier');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
it('redirects to team page when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/zapier');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/zapier');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects to team page when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
authenticateSession(application);
await authenticateSession();
await visit('/settings/integrations/zapier');
expect(currentURL(), 'currentURL').to.equal('/team');
});
describe('when logged in', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it('it loads', async function () {

View file

@ -1,27 +1,22 @@
import destroyApp from '../helpers/destroy-app';
import moment from 'moment';
import startApp from '../helpers/start-app';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {Response} from 'ember-cli-mirage';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from '../helpers/ember-simple-auth';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {blur, click, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../helpers/visit';
describe('Acceptance: Setup', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects if already authenticated', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
await authenticateSession(application);
await authenticateSession();
await visit('/setup/one');
expect(currentURL()).to.equal('/');
@ -35,7 +30,7 @@ describe('Acceptance: Setup', function () {
it('redirects to signin if already set up', async function () {
// mimick an already setup blog
server.get('/authentication/setup/', function () {
this.server.get('/authentication/setup/', function () {
return {
setup: [
{status: true}
@ -43,7 +38,7 @@ describe('Acceptance: Setup', function () {
};
});
await invalidateSession(application);
await invalidateSession();
await visit('/setup');
expect(currentURL()).to.equal('/signin');
@ -52,7 +47,7 @@ describe('Acceptance: Setup', function () {
describe('with a new blog', function () {
beforeEach(function () {
// mimick a new blog
server.get('/authentication/setup/', function () {
this.server.get('/authentication/setup/', function () {
return {
setup: [
{status: false}
@ -62,8 +57,8 @@ describe('Acceptance: Setup', function () {
});
it('has a successful happy path', async function () {
invalidateSession(application);
server.loadFixtures('roles');
await invalidateSession();
this.server.loadFixtures('roles');
await visit('/setup');
@ -72,16 +67,15 @@ describe('Acceptance: Setup', function () {
.to.equal('/setup/one');
// it highlights first step
expect(find('.gh-flow-nav .step:first-of-type').hasClass('active'))
.to.be.true;
expect(find('.gh-flow-nav .step:nth-of-type(2)').hasClass('active'))
.to.be.false;
expect(find('.gh-flow-nav .step:nth-of-type(3)').hasClass('active'))
.to.be.false;
let stepIcons = findAll('.gh-flow-nav .step');
expect(stepIcons.length, 'sanity check: three steps').to.equal(3);
expect(stepIcons[0], 'first step').to.have.class('active');
expect(stepIcons[1], 'second step').to.not.have.class('active');
expect(stepIcons[2], 'third step').to.not.have.class('active');
// it displays download count (count increments for each ajax call
// and polling is disabled in testing so our count should be "1"
expect(find('.gh-flow-content em').text().trim()).to.equal('1');
expect(find('.gh-flow-content em').textContent.trim()).to.equal('1');
await click('.gh-btn-green');
@ -92,21 +86,21 @@ describe('Acceptance: Setup', function () {
// email field is focused by default
// NOTE: $('x').is(':focus') doesn't work in phantomjs CLI runner
// https://github.com/ariya/phantomjs/issues/10427
expect(find('[data-test-blog-title-input]').get(0) === document.activeElement, 'blog title has focus')
expect(findAll('[data-test-blog-title-input]')[0] === document.activeElement, 'blog title has focus')
.to.be.true;
await click('.gh-btn-green');
// it marks fields as invalid
expect(find('.form-group.error').length, 'number of invalid fields')
expect(findAll('.form-group.error').length, 'number of invalid fields')
.to.equal(4);
// it displays error messages
expect(find('.error .response').length, 'number of in-line validation messages')
expect(findAll('.error .response').length, 'number of in-line validation messages')
.to.equal(4);
// it displays main error
expect(find('.main-error').length, 'main error is displayed')
expect(findAll('.main-error').length, 'main error is displayed')
.to.equal(1);
// enter valid details and submit
@ -121,14 +115,14 @@ describe('Acceptance: Setup', function () {
.to.equal('/setup/three');
// submit button is "disabled"
expect(find('button[type="submit"]').hasClass('gh-btn-green'), 'invite button with no emails is white')
expect(find('button[type="submit"]').classList.contains('gh-btn-green'), 'invite button with no emails is white')
.to.be.false;
// fill in a valid email
await fillIn('[name="users"]', 'new-user@example.com');
// submit button is "enabled"
expect(find('button[type="submit"]').hasClass('gh-btn-green'), 'invite button is green with valid email address')
expect(find('button[type="submit"]').classList.contains('gh-btn-green'), 'invite button is green with valid email address')
.to.be.true;
// submit the invite form
@ -139,17 +133,17 @@ describe('Acceptance: Setup', function () {
.to.equal('/');
// it displays success alert
expect(find('.gh-alert-green').length, 'number of success alerts')
expect(findAll('.gh-alert-green').length, 'number of success alerts')
.to.equal(1);
});
it('handles validation errors in step 2', async function () {
let postCount = 0;
invalidateSession(application);
server.loadFixtures('roles');
await invalidateSession();
this.server.loadFixtures('roles');
server.post('/authentication/setup', function () {
this.server.post('/authentication/setup', function () {
postCount += 1;
// validation error
@ -174,7 +168,7 @@ describe('Acceptance: Setup', function () {
await click('.gh-btn-green');
// non-server validation
expect(find('.main-error').text().trim(), 'error text')
expect(find('.main-error').textContent.trim(), 'error text')
.to.not.be.empty;
await fillIn('[data-test-email-input]', 'test@example.com');
@ -185,22 +179,22 @@ describe('Acceptance: Setup', function () {
// first post - simulated validation error
await click('.gh-btn-green');
expect(find('.main-error').text().trim(), 'error text')
expect(find('.main-error').textContent.trim(), 'error text')
.to.equal('Server response message');
// second post - simulated server error
await click('.gh-btn-green');
expect(find('.main-error').text().trim(), 'error text')
expect(find('.main-error').textContent.trim(), 'error text')
.to.be.empty;
expect(find('.gh-alert-red').length, 'number of alerts')
expect(findAll('.gh-alert-red').length, 'number of alerts')
.to.equal(1);
});
it('handles invalid origin error on step 2', async function () {
// mimick the API response for an invalid origin
server.post('/session', function () {
this.server.post('/session', function () {
return new Response(401, {}, {
errors: [
{
@ -211,8 +205,8 @@ describe('Acceptance: Setup', function () {
});
});
invalidateSession(application);
server.loadFixtures('roles');
await invalidateSession();
this.server.loadFixtures('roles');
await visit('/setup/two');
await fillIn('[data-test-email-input]', 'test@example.com');
@ -222,10 +216,10 @@ describe('Acceptance: Setup', function () {
await click('.gh-btn-green');
// button should not be spinning
expect(find('.gh-btn-green .spinner').length, 'button has spinner')
expect(findAll('.gh-btn-green .spinner').length, 'button has spinner')
.to.equal(0);
// we should show an error message
expect(find('.main-error').text(), 'error text')
expect(find('.main-error').textContent, 'error text')
.to.have.string('Access Denied from url: unknown.com. Please use the url configured in config.js.');
});
@ -234,10 +228,10 @@ describe('Acceptance: Setup', function () {
let postCount = 0;
let button, formGroup;
invalidateSession(application);
server.loadFixtures('roles');
await invalidateSession();
this.server.loadFixtures('roles');
server.post('/invites/', function ({invites}) {
this.server.post('/invites/', function ({invites}) {
let attrs = this.normalizedRequestAttrs();
postCount += 1;
@ -278,66 +272,66 @@ describe('Acceptance: Setup', function () {
formGroup = find('.gh-flow-invite .form-group');
button = find('.gh-flow-invite button[type="submit"]');
expect(formGroup.hasClass('error'), 'default field has error class')
.to.be.false;
expect(formGroup, 'default field has error class')
.to.not.have.class('error');
expect(button.text().trim(), 'default button text')
.to.equal('Invite some users');
expect(button.textContent, 'default button text')
.to.have.string('Invite some users');
expect(button.hasClass('gh-btn-minor'), 'default button is disabled')
.to.be.true;
expect(button, 'default button is disabled')
.to.have.class('gh-btn-minor');
// no users submitted state
await click('.gh-flow-invite button[type="submit"]');
expect(formGroup.hasClass('error'), 'no users submitted field has error class')
.to.be.true;
expect(formGroup, 'no users submitted field has error class')
.to.have.class('error');
expect(button.text().trim(), 'no users submitted button text')
.to.equal('No users to invite');
expect(button.textContent, 'no users submitted button text')
.to.have.string('No users to invite');
expect(button.hasClass('gh-btn-minor'), 'no users submitted button is disabled')
.to.be.true;
expect(button, 'no users submitted button is disabled')
.to.have.class('gh-btn-minor');
// single invalid email
await fillIn(input, 'invalid email');
await triggerEvent(input, 'blur');
await blur(input);
expect(formGroup.hasClass('error'), 'invalid field has error class')
.to.be.true;
expect(formGroup, 'invalid field has error class')
.to.have.class('error');
expect(button.text().trim(), 'single invalid button text')
.to.equal('1 invalid email address');
expect(button.textContent, 'single invalid button text')
.to.have.string('1 invalid email address');
expect(button.hasClass('gh-btn-minor'), 'invalid email button is disabled')
.to.be.true;
expect(button, 'invalid email button is disabled')
.to.have.class('gh-btn-minor');
// multiple invalid emails
await fillIn(input, 'invalid email\nanother invalid address');
await triggerEvent(input, 'blur');
await blur(input);
expect(button.text().trim(), 'multiple invalid button text')
.to.equal('2 invalid email addresses');
expect(button.textContent, 'multiple invalid button text')
.to.have.string('2 invalid email addresses');
// single valid email
await fillIn(input, 'invited@example.com');
await triggerEvent(input, 'blur');
await blur(input);
expect(formGroup.hasClass('error'), 'valid field has error class')
.to.be.false;
expect(formGroup, 'valid field has error class')
.to.not.have.class('error');
expect(button.text().trim(), 'single valid button text')
.to.equal('Invite 1 user');
expect(button.textContent, 'single valid button text')
.to.have.string('Invite 1 user');
expect(button.hasClass('gh-btn-green'), 'valid email button is enabled')
.to.be.true;
expect(button, 'valid email button is enabled')
.to.have.class('gh-btn-green');
// multiple valid emails
await fillIn(input, 'invited1@example.com\ninvited2@example.com');
await triggerEvent(input, 'blur');
await blur(input);
expect(button.text().trim(), 'multiple valid button text')
.to.equal('Invite 2 users');
expect(button.textContent, 'multiple valid button text')
.to.have.string('Invite 2 users');
// submit invitations with simulated failure on 1 invite
await click('.gh-btn-green');
@ -347,11 +341,11 @@ describe('Acceptance: Setup', function () {
.to.equal('/');
// it displays success alert
expect(find('.gh-alert-green').length, 'number of success alerts')
expect(findAll('.gh-alert-green').length, 'number of success alerts')
.to.equal(1);
// it displays failure alert
expect(find('.gh-alert-red').length, 'number of failure alerts')
expect(findAll('.gh-alert-red').length, 'number of failure alerts')
.to.equal(1);
});
});

View file

@ -1,31 +1,25 @@
import destroyApp from '../helpers/destroy-app';
import startApp from '../helpers/start-app';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {Response} from 'ember-cli-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {
afterEach,
beforeEach,
describe,
it
} from 'mocha';
import {authenticateSession, invalidateSession} from '../helpers/ember-simple-auth';
import {click, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../helpers/visit';
describe('Acceptance: Signin', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects if already authenticated', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
await authenticateSession(application);
await authenticateSession();
await visit('/signin');
expect(currentURL(), 'current url').to.equal('/');
@ -33,10 +27,10 @@ describe('Acceptance: Signin', function () {
describe('when attempting to signin', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role], slug: 'test-user'});
server.post('/session', function (schema, {requestBody}) {
this.server.post('/session', function (schema, {requestBody}) {
let {
username,
password
@ -58,22 +52,22 @@ describe('Acceptance: Signin', function () {
});
it('errors correctly', async function () {
await invalidateSession(application);
await invalidateSession();
await visit('/signin');
expect(currentURL(), 'signin url').to.equal('/signin');
expect(find('input[name="identification"]').length, 'email input field')
expect(findAll('input[name="identification"]').length, 'email input field')
.to.equal(1);
expect(find('input[name="password"]').length, 'password input field')
expect(findAll('input[name="password"]').length, 'password input field')
.to.equal(1);
await click('.gh-btn-blue');
expect(find('.form-group.error').length, 'number of invalid fields')
expect(findAll('.form-group.error').length, 'number of invalid fields')
.to.equal(2);
expect(find('.main-error').length, 'main error is displayed')
expect(findAll('.main-error').length, 'main error is displayed')
.to.equal(1);
await fillIn('[name="identification"]', 'test@example.com');
@ -82,15 +76,15 @@ describe('Acceptance: Signin', function () {
expect(currentURL(), 'current url').to.equal('/signin');
expect(find('.main-error').length, 'main error is displayed')
expect(findAll('.main-error').length, 'main error is displayed')
.to.equal(1);
expect(find('.main-error').text().trim(), 'main error text')
expect(find('.main-error').textContent.trim(), 'main error text')
.to.equal('Invalid Password');
});
it('submits successfully', async function () {
invalidateSession(application);
invalidateSession();
await visit('/signin');
expect(currentURL(), 'current url').to.equal('/signin');

View file

@ -1,25 +1,18 @@
import destroyApp from '../helpers/destroy-app';
import startApp from '../helpers/start-app';
import {
afterEach,
beforeEach,
describe,
it
} from 'mocha';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {blur, click, currentRouteName, fillIn, find} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../helpers/visit';
describe('Acceptance: Signup', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('can signup successfully', async function () {
let server = this.server;
server.get('/authentication/invitation', function () {
return {
invitation: [{valid: true}]
@ -48,118 +41,156 @@ describe('Acceptance: Signup', function () {
// "1470346017929|kevin+test2@ghost.org|2cDnQc3g7fQTj9nNK4iGPSGfvomkLdXf68FuWgS66Ug="
await visit('/signup/MTQ3MDM0NjAxNzkyOXxrZXZpbit0ZXN0MkBnaG9zdC5vcmd8MmNEblFjM2c3ZlFUajluTks0aUdQU0dmdm9ta0xkWGY2OEZ1V2dTNjZVZz0');
expect(currentPath()).to.equal('signup');
expect(currentRouteName()).to.equal('signup');
// email address should be pre-filled and disabled
expect(
find('input[name="email"]').val(),
find('input[name="email"]').value,
'email field value'
).to.equal('kevin+test2@ghost.org');
expect(
find('input[name="email"]').is(':disabled'),
find('input[name="email"]').matches(':disabled'),
'email field is disabled'
).to.be.true;
// focus out in Name field triggers inline error
await triggerEvent('input[name="name"]', 'blur');
await blur('input[name="name"]');
expect(
find('input[name="name"]').closest('.form-group').hasClass('error'),
find('input[name="name"]').closest('.form-group'),
'name field group has error class when empty'
).to.be.true;
).to.have.class('error');
expect(
find('input[name="name"]').closest('.form-group').find('.response').text().trim(),
find('input[name="name"]').closest('.form-group').querySelector('.response').textContent,
'name inline-error text'
).to.match(/Please enter a name/);
).to.have.string('Please enter a name');
// entering text in Name field clears error
await fillIn('input[name="name"]', 'Test User');
await triggerEvent('input[name="name"]', 'blur');
await blur('input[name="name"]');
expect(
find('input[name="name"]').closest('.form-group').hasClass('error'),
find('input[name="name"]').closest('.form-group'),
'name field loses error class after text input'
).to.be.false;
).to.not.have.class('error');
expect(
find('input[name="name"]').closest('.form-group').find('.response').text().trim(),
find('input[name="name"]').closest('.form-group').querySelector('.response').textContent.trim(),
'name field error is removed after text input'
).to.equal('');
).to.be.empty;
// check password validation
// focus out in password field triggers inline error
// no password
await triggerEvent('input[name="password"]', 'blur');
await click('input[name="password"]');
await blur();
expect(
find('input[name="password"]').closest('.form-group').hasClass('error'),
find('input[name="password"]').closest('.form-group'),
'password field group has error class when empty'
).to.be.true;
).to.have.class('error');
expect(
find('input[name="password"]').closest('.form-group').find('.response').text().trim(),
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent,
'password field error text'
).to.match(/must be at least 10 characters/);
).to.have.string('must be at least 10 characters');
// password too short
await fillIn('input[name="password"]', 'short');
await triggerEvent('input[name="password"]', 'blur');
await blur('input[name="password"]');
expect(
find('input[name="password"]').closest('.form-group').find('.response').text().trim(),
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent,
'password field error text'
).to.match(/must be at least 10 characters/);
).to.have.string('must be at least 10 characters');
// password must not be a bad password
await fillIn('input[name="password"]', '1234567890');
await triggerEvent('input[name="password"]', 'blur');
await blur('input[name="password"]');
expect(
find('input[name="password"]').closest('.form-group').find('.response').text().trim(),
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent,
'password field error text'
).to.match(/you cannot use an insecure password/);
).to.have.string('you cannot use an insecure password');
// password must not be a disallowed password
await fillIn('input[name="password"]', 'password99');
await triggerEvent('input[name="password"]', 'blur');
await blur('input[name="password"]');
expect(
find('input[name="password"]').closest('.form-group').find('.response').text().trim(),
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent,
'password field error text'
).to.match(/you cannot use an insecure password/);
).to.have.string('you cannot use an insecure password');
// password must not have repeating characters
await fillIn('input[name="password"]', '2222222222');
await triggerEvent('input[name="password"]', 'blur');
await blur('input[name="password"]');
expect(
find('input[name="password"]').closest('.form-group').find('.response').text().trim(),
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent,
'password field error text'
).to.match(/you cannot use an insecure password/);
).to.have.string('you cannot use an insecure password');
// entering valid text in Password field clears error
await fillIn('input[name="password"]', 'thisissupersafe');
await triggerEvent('input[name="password"]', 'blur');
await blur('input[name="password"]');
expect(
find('input[name="password"]').closest('.form-group').hasClass('error'),
find('input[name="password"]').closest('.form-group'),
'password field loses error class after text input'
).to.be.false;
).to.not.have.class('error');
expect(
find('input[name="password"]').closest('.form-group').find('.response').text().trim(),
find('input[name="password"]').closest('.form-group').querySelector('.response').textContent.trim(),
'password field error is removed after text input'
).to.equal('');
// submitting sends correct details and redirects to content screen
await click('.gh-btn-green');
expect(currentPath()).to.equal('posts.index');
expect(currentRouteName()).to.equal('posts.index');
});
it('redirects if already logged in');
it('redirects with alert on invalid token');
it('redirects with alert on non-existant or expired token');
it('redirects if already logged in', async function () {
this.server.get('/authentication/invitation', function () {
return {
invitation: [{valid: true}]
};
});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
await authenticateSession();
// token details:
// "1470346017929|kevin+test2@ghost.org|2cDnQc3g7fQTj9nNK4iGPSGfvomkLdXf68FuWgS66Ug="
await visit('/signup/MTQ3MDM0NjAxNzkyOXxrZXZpbit0ZXN0MkBnaG9zdC5vcmd8MmNEblFjM2c3ZlFUajluTks0aUdQU0dmdm9ta0xkWGY2OEZ1V2dTNjZVZz0');
expect(currentRouteName()).to.equal('posts.index');
expect(find('.gh-alert-content').textContent).to.have.string('sign out to register');
});
it('redirects with alert on invalid token', async function () {
await invalidateSession();
await visit('/signup/---invalid---');
expect(currentRouteName()).to.equal('signin');
expect(find('.gh-alert-content').textContent).to.have.string('Invalid token');
});
it('redirects with alert on non-existant or expired token', async function () {
this.server.get('/authentication/invitation', function () {
return {
invitation: [{valid: false}]
};
});
await invalidateSession();
await visit('/signup/expired');
expect(currentRouteName()).to.equal('signin');
expect(find('.gh-alert-content').textContent).to.have.string('not exist');
});
});

View file

@ -1,79 +1,76 @@
import destroyApp from '../helpers/destroy-app';
import startApp from '../helpers/start-app';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {click, currentRouteName, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
import {expect} from 'chai';
import {fileUpload} from '../helpers/file-upload';
import {findAllWithText, findWithText} from '../helpers/find';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../helpers/visit';
describe('Acceptance: Subscribers', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/subscribers');
expect(currentURL()).to.equal('/signin');
});
it('redirects editors to posts', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role]});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role]});
authenticateSession(application);
await authenticateSession();
await visit('/subscribers');
expect(currentURL()).to.equal('/');
expect(find('[data-test-nav="subscribers"]').length, 'sidebar link is visible')
.to.equal(0);
expect(find('[data-test-nav="subscribers"]'), 'sidebar link')
.to.not.exist;
});
it('redirects authors to posts', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role]});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role]});
authenticateSession(application);
await authenticateSession();
await visit('/subscribers');
expect(currentURL()).to.equal('/');
expect(find('[data-test-nav="subscribers"]').length, 'sidebar link is visible')
.to.equal(0);
expect(find('[data-test-nav="subscribers"]'), 'sidebar link')
.to.not.exist;
});
it('redirects contributors to posts', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role]});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role]});
authenticateSession(application);
await authenticateSession();
await visit('/subscribers');
expect(currentURL()).to.equal('/');
expect(find('[data-test-nav="subscribers"]').length, 'sidebar link is visible')
.to.equal(0);
expect(find('[data-test-nav="subscribers"]'), 'sidebar link')
.to.not.exist;
});
describe('an admin', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
beforeEach(async function () {
let role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
return authenticateSession(application);
return await authenticateSession();
});
it('can manage subscribers', async function () {
server.createList('subscriber', 40);
this.server.createList('subscriber', 40);
await visit('/');
await click('[data-test-nav="subscribers"]');
// it navigates to the correct page
expect(currentPath()).to.equal('subscribers.index');
expect(currentRouteName()).to.equal('subscribers.index');
// it has correct page title
expect(document.title, 'page title')
@ -83,33 +80,33 @@ describe('Acceptance: Subscribers', function () {
// TODO: latest ember-in-viewport causes infinite scroll issues with
// FF here where it loads two pages straight away so we need to check
// if rows are greater than or equal to a single page
expect(find('.subscribers-table .lt-body .lt-row').length, 'number of subscriber rows')
expect(findAll('.subscribers-table .lt-body .lt-row').length, 'number of subscriber rows')
.to.be.at.least(30);
// it shows the total number of subscribers
expect(find('[data-test-total-subscribers]').text().trim(), 'displayed subscribers total')
expect(find('[data-test-total-subscribers]').textContent.trim(), 'displayed subscribers total')
.to.equal('(40)');
// it defaults to sorting by created_at desc
let [lastRequest] = server.pretender.handledRequests.slice(-1);
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.order).to.equal('created_at desc');
let createdAtHeader = find('.subscribers-table th:contains("Subscription Date")');
expect(createdAtHeader.hasClass('is-sorted'), 'createdAt column is sorted')
.to.be.true;
expect(createdAtHeader.find('.gh-icon-descending').length, 'createdAt column has descending icon')
.to.equal(1);
let createdAtHeader = findWithText('.subscribers-table th', 'Subscription Date');
expect(createdAtHeader, 'createdAt column is sorted')
.to.have.class('is-sorted');
expect(createdAtHeader.querySelectorAll('.gh-icon-descending'), 'createdAt column has descending icon')
.to.exist;
// click the column to re-order
await click('th:contains("Subscription Date")');
await click(findWithText('th', 'Subscription Date'));
// it flips the directions and re-fetches
[lastRequest] = server.pretender.handledRequests.slice(-1);
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.order).to.equal('created_at asc');
createdAtHeader = find('.subscribers-table th:contains("Subscription Date")');
expect(createdAtHeader.find('.gh-icon-ascending').length, 'createdAt column has ascending icon')
.to.equal(1);
createdAtHeader = findWithText('.subscribers-table th', 'Subscription Date');
expect(createdAtHeader.querySelector('.gh-icon-ascending'), 'createdAt column has ascending icon')
.to.exist;
// TODO: scroll test disabled as ember-light-table doesn't calculate
// the scroll trigger element's positioning against the scroll
@ -126,31 +123,31 @@ describe('Acceptance: Subscribers', function () {
// .to.equal(40);
// click the add subscriber button
await click('.gh-btn:contains("Add Subscriber")');
await click('[data-test-link="add-subscriber"]');
// it displays the add subscriber modal
expect(find('.fullscreen-modal').length, 'add subscriber modal displayed')
.to.equal(1);
expect(find('[data-test-modal="new-subscriber"]'), 'add subscriber modal displayed')
.to.exist;
// cancel the modal
await click('.fullscreen-modal .gh-btn:contains("Cancel")');
await click('[data-test-button="cancel-new-subscriber"]');
// it closes the add subscriber modal
expect(find('.fullscreen-modal').length, 'add subscriber modal displayed after cancel')
.to.equal(0);
expect(find('[data-test-modal]'), 'add subscriber modal displayed after cancel')
.to.not.exist;
// save a new subscriber
await click('.gh-btn:contains("Add Subscriber")');
await fillIn('.fullscreen-modal input[name="email"]', 'test@example.com');
await click('.fullscreen-modal .gh-btn:contains("Add")');
await click('[data-test-link="add-subscriber"]');
await fillIn('[data-test-input="new-subscriber-email"]', 'test@example.com');
await click('[data-test-button="create-subscriber"]');
// the add subscriber modal is closed
expect(find('.fullscreen-modal').length, 'add subscriber modal displayed after save')
.to.equal(0);
expect(find('[data-test-modal]'), 'add subscriber modal displayed after save')
.to.not.exist;
// the subscriber is added to the table
expect(find('.subscribers-table .lt-body .lt-row:first-of-type .lt-cell:first-of-type').text().trim(), 'first email in list after addition')
.to.equal('test@example.com');
expect(find('.subscribers-table .lt-body .lt-row:first-of-type .lt-cell:first-of-type'), 'first email in list after addition')
.to.contain.text('test@example.com');
// the table is scrolled to the top
// TODO: implement scroll to new record after addition
@ -158,90 +155,90 @@ describe('Acceptance: Subscribers', function () {
// .to.equal(0);
// the subscriber total is updated
expect(find('[data-test-total-subscribers]').text().trim(), 'subscribers total after addition')
.to.equal('(41)');
expect(find('[data-test-total-subscribers]'), 'subscribers total after addition')
.to.have.trimmed.text('(41)');
// saving a duplicate subscriber
await click('.gh-btn:contains("Add Subscriber")');
await fillIn('.fullscreen-modal input[name="email"]', 'test@example.com');
await click('.fullscreen-modal .gh-btn:contains("Add")');
await click('[data-test-link="add-subscriber"]');
await fillIn('[data-test-input="new-subscriber-email"]', 'test@example.com');
await click('[data-test-button="create-subscriber"]');
// the validation error is displayed
expect(find('.fullscreen-modal .error .response').text().trim(), 'duplicate email validation')
.to.equal('Email already exists.');
expect(find('[data-test-error="new-subscriber-email"]'), 'duplicate email validation')
.to.have.trimmed.text('Email already exists.');
// the subscriber is not added to the table
expect(find('.lt-cell:contains(test@example.com)').length, 'number of "test@example.com rows"')
expect(findAllWithText('.lt-cell', 'test@example.com').length, 'number of "test@example.com rows"')
.to.equal(1);
// the subscriber total is unchanged
expect(find('[data-test-total-subscribers]').text().trim(), 'subscribers total after failed add')
.to.equal('(41)');
expect(find('[data-test-total-subscribers]'), 'subscribers total after failed add')
.to.have.trimmed.text('(41)');
// deleting a subscriber
await click('.fullscreen-modal .gh-btn:contains("Cancel")');
await click('[data-test-button="cancel-new-subscriber"]');
await click('.subscribers-table tbody tr:first-of-type button:last-of-type');
// it displays the delete subscriber modal
expect(find('.fullscreen-modal').length, 'delete subscriber modal displayed')
.to.equal(1);
expect(find('[data-test-modal="delete-subscriber"]'), 'delete subscriber modal displayed')
.to.exist;
// cancel the modal
await click('.fullscreen-modal .gh-btn:contains("Cancel")');
await click('[data-test-button="cancel-delete-subscriber"]');
// it closes the add subscriber modal
expect(find('.fullscreen-modal').length, 'delete subscriber modal displayed after cancel')
.to.equal(0);
expect(find('[data-test-modal]'), 'delete subscriber modal displayed after cancel')
.to.not.exist;
await click('.subscribers-table tbody tr:first-of-type button:last-of-type');
await click('.fullscreen-modal .gh-btn:contains("Delete")');
await click('[data-test-button="confirm-delete-subscriber"]');
// the add subscriber modal is closed
expect(find('.fullscreen-modal').length, 'delete subscriber modal displayed after confirm')
.to.equal(0);
expect(find('[data-test-modal]'), 'delete subscriber modal displayed after confirm')
.to.not.exist;
// the subscriber is removed from the table
expect(find('.subscribers-table .lt-body .lt-row:first-of-type .lt-cell:first-of-type').text().trim(), 'first email in list after addition')
.to.not.equal('test@example.com');
expect(find('.subscribers-table .lt-body .lt-row:first-of-type .lt-cell:first-of-type'), 'first email in list after addition')
.to.not.have.trimmed.text('test@example.com');
// the subscriber total is updated
expect(find('[data-test-total-subscribers]').text().trim(), 'subscribers total after addition')
.to.equal('(40)');
expect(find('[data-test-total-subscribers]'), 'subscribers total after addition')
.to.have.trimmed.text('(40)');
// click the import subscribers button
await click('[data-test-link="import-csv"]');
// it displays the import subscribers modal
expect(find('.fullscreen-modal').length, 'import subscribers modal displayed')
.to.equal(1);
expect(find('.fullscreen-modal input[type="file"]').length, 'import modal contains file input')
.to.equal(1);
expect(find('[data-test-modal="import-subscribers"]'), 'import subscribers modal displayed')
.to.exist;
expect(find('.fullscreen-modal input[type="file"]'), 'import modal contains file input')
.to.exist;
// cancel the modal
await click('.fullscreen-modal .gh-btn:contains("Cancel")');
await click('[data-test-button="close-import-subscribers"]');
// it closes the import subscribers modal
expect(find('.fullscreen-modal').length, 'import subscribers modal displayed after cancel')
.to.equal(0);
expect(find('[data-test-modal]'), 'import subscribers modal displayed after cancel')
.to.not.exist;
await click('[data-test-link="import-csv"]');
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'test.csv'});
// modal title changes
expect(find('.fullscreen-modal h1').text().trim(), 'import modal title after import')
.to.equal('Import Successful');
expect(find('[data-test-modal="import-subscribers"] h1'), 'import modal title after import')
.to.have.trimmed.text('Import Successful');
// modal button changes
expect(find('.fullscreen-modal .modal-footer button').text().trim(), 'import modal button text after import')
.to.equal('Close');
expect(find('[data-test-button="close-import-subscribers"]'), 'import modal button text after import')
.to.have.trimmed.text('Close');
// subscriber total is updated
expect(find('[data-test-total-subscribers]').text().trim(), 'subscribers total after import')
.to.equal('(90)');
expect(find('[data-test-total-subscribers]'), 'subscribers total after import')
.to.have.trimmed.text('(90)');
// TODO: re-enable once bug in ember-light-table that triggers second page load is fixed
// table is reset
// [lastRequest] = server.pretender.handledRequests.slice(-1);
// [lastRequest] = this.server.pretender.handledRequests.slice(-1);
// expect(lastRequest.url, 'endpoint requested after import')
// .to.match(/\/subscribers\/\?/);
// expect(lastRequest.queryParams.page, 'page requested after import')

View file

@ -1,63 +1,69 @@
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import destroyApp from '../helpers/destroy-app';
import moment from 'moment';
import startApp from '../helpers/start-app';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import windowProxy from 'ghost-admin/utils/window-proxy';
import {Response} from 'ember-cli-mirage';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from '../helpers/ember-simple-auth';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {
blur,
click,
currentRouteName,
currentURL,
fillIn,
find,
findAll,
focus,
triggerEvent,
triggerKeyEvent
} from '@ember/test-helpers';
import {errorOverride, errorReset} from '../helpers/adapter-error';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {visit} from '../helpers/visit';
describe('Acceptance: Team', function () {
let application;
beforeEach(function () {
application = startApp();
});
afterEach(function () {
destroyApp(application);
});
let hooks = setupApplicationTest();
setupMirage(hooks);
it('redirects to signin when not authenticated', async function () {
invalidateSession(application);
await invalidateSession();
await visit('/team');
expect(currentURL()).to.equal('/signin');
});
it('redirects correctly when authenticated as contributor', async function () {
let role = server.create('role', {name: 'Contributor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Contributor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
server.create('user', {slug: 'no-access'});
this.server.create('user', {slug: 'no-access'});
authenticateSession(application);
await authenticateSession();
await visit('/team/no-access');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects correctly when authenticated as author', async function () {
let role = server.create('role', {name: 'Author'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [role], slug: 'test-user'});
server.create('user', {slug: 'no-access'});
this.server.create('user', {slug: 'no-access'});
authenticateSession(application);
await authenticateSession();
await visit('/team/no-access');
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
});
it('redirects correctly when authenticated as editor', async function () {
let role = server.create('role', {name: 'Editor'});
server.create('user', {roles: [role], slug: 'test-user'});
let role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
server.create('user', {slug: 'no-access'});
this.server.create('user', {slug: 'no-access'});
authenticateSession(application);
await authenticateSession();
await visit('/team/no-access');
expect(currentURL(), 'currentURL').to.equal('/team');
@ -66,24 +72,24 @@ describe('Acceptance: Team', function () {
describe('when logged in as admin', function () {
let admin, adminRole, suspendedUser;
beforeEach(function () {
server.loadFixtures('roles');
adminRole = server.schema.roles.find(1);
beforeEach(async function () {
this.server.loadFixtures('roles');
adminRole = this.server.schema.roles.find(1);
admin = server.create('user', {email: 'admin@example.com', roles: [adminRole]});
admin = this.server.create('user', {email: 'admin@example.com', roles: [adminRole]});
// add an expired invite
server.create('invite', {expires: moment.utc().subtract(1, 'day').valueOf(), role: adminRole});
this.server.create('invite', {expires: moment.utc().subtract(1, 'day').valueOf(), role: adminRole});
// add a suspended user
suspendedUser = server.create('user', {email: 'suspended@example.com', roles: [adminRole], status: 'inactive'});
suspendedUser = this.server.create('user', {email: 'suspended@example.com', roles: [adminRole], status: 'inactive'});
return authenticateSession(application);
return await authenticateSession();
});
it('it renders and navigates correctly', async function () {
let user1 = server.create('user');
let user2 = server.create('user');
let user1 = this.server.create('user');
let user2 = this.server.create('user');
await visit('/team');
@ -95,7 +101,7 @@ describe('Acceptance: Team', function () {
// it shows active users in active section
expect(
find('[data-test-active-users] [data-test-user-id]').length,
findAll('[data-test-active-users] [data-test-user-id]').length,
'number of active users'
).to.equal(3);
expect(
@ -110,7 +116,7 @@ describe('Acceptance: Team', function () {
// it shows suspended users in suspended section
expect(
find('[data-test-suspended-users] [data-test-user-id]').length,
findAll('[data-test-suspended-users] [data-test-user-id]').length,
'number of suspended users'
).to.equal(1);
expect(
@ -127,7 +133,7 @@ describe('Acceptance: Team', function () {
// view title should exist and be linkable and active
expect(
find('[data-test-screen-title] a[href="/ghost/team"]').hasClass('active'),
find('[data-test-screen-title] a[href="/ghost/team"]').classList.contains('active'),
'has linkable url back to team main page'
).to.be.true;
@ -142,29 +148,29 @@ describe('Acceptance: Team', function () {
// invite user button exists
expect(
find('.view-actions .gh-btn-green').text().trim(),
find('.view-actions .gh-btn-green').textContent.trim(),
'invite people button text'
).to.equal('Invite People');
// existing users are listed
expect(
find('[data-test-user-id]').length,
findAll('[data-test-user-id]').length,
'initial number of active users'
).to.equal(2);
expect(
find('[data-test-user-id="1"] [data-test-role-name]').text().trim(),
find('[data-test-user-id="1"] [data-test-role-name]').textContent.trim(),
'active user\'s role label'
).to.equal('Administrator');
// existing invites are shown
expect(
find('[data-test-invite-id]').length,
findAll('[data-test-invite-id]').length,
'initial number of invited users'
).to.equal(1);
expect(
find('[data-test-invite-id="1"] [data-test-invite-description]').text(),
find('[data-test-invite-id="1"] [data-test-invite-description]').textContent,
'expired invite description'
).to.match(/expired/);
@ -172,14 +178,14 @@ describe('Acceptance: Team', function () {
await click('[data-test-invite-id="1"] [data-test-revoke-button]');
expect(
find('[data-test-invite-id]').length,
findAll('[data-test-invite-id]').length,
'initial number of invited users'
).to.equal(0);
// click the invite people button
await click('.view-actions .gh-btn-green');
let roleOptions = find('.fullscreen-modal select[name="role"] option');
let roleOptions = findAll('.fullscreen-modal select[name="role"] option');
function checkOwnerExists() {
for (let i in roleOptions) {
@ -201,13 +207,13 @@ describe('Acceptance: Team', function () {
// modal is displayed
expect(
find('.fullscreen-modal h1').text().trim(),
find('.fullscreen-modal h1').textContent.trim(),
'correct modal is displayed'
).to.equal('Invite a New User');
// number of roles is correct
expect(
find('.fullscreen-modal select[name="role"] option').length,
findAll('.fullscreen-modal select[name="role"] option').length,
'number of selectable roles'
).to.equal(3);
@ -220,34 +226,34 @@ describe('Acceptance: Team', function () {
// modal closes
expect(
find('.fullscreen-modal').length,
findAll('[data-test-modal]').length,
'number of modals after sending invite'
).to.equal(0);
// invite is displayed, has correct e-mail + role
expect(
find('[data-test-invite-id]').length,
findAll('[data-test-invite-id]').length,
'number of invites after first invite'
).to.equal(1);
expect(
find('[data-test-invite-id="2"] [data-test-email]').text().trim(),
find('[data-test-invite-id="2"] [data-test-email]').textContent.trim(),
'displayed email of first invite'
).to.equal('invite1@example.com');
expect(
find('[data-test-invite-id="2"] [data-test-role-name]').text().trim(),
find('[data-test-invite-id="2"] [data-test-role-name]').textContent.trim(),
'displayed role of first invite'
).to.equal('Author');
expect(
find('[data-test-invite-id="2"] [data-test-invite-description]').text(),
find('[data-test-invite-id="2"] [data-test-invite-description]').textContent,
'new invite description'
).to.match(/expires/);
// number of users is unchanged
expect(
find('[data-test-user-id]').length,
findAll('[data-test-user-id]').length,
'number of active users after first invite'
).to.equal(2);
@ -259,18 +265,18 @@ describe('Acceptance: Team', function () {
// number of invites increases
expect(
find('[data-test-invite-id]').length,
findAll('[data-test-invite-id]').length,
'number of invites after second invite'
).to.equal(2);
// invite has correct e-mail + role
expect(
find('[data-test-invite-id="3"] [data-test-email]').text().trim(),
find('[data-test-invite-id="3"] [data-test-email]').textContent.trim(),
'displayed email of second invite'
).to.equal('invite2@example.com');
expect(
find('[data-test-invite-id="3"] [data-test-role-name]').text().trim(),
find('[data-test-invite-id="3"] [data-test-role-name]').textContent.trim(),
'displayed role of second invite'
).to.equal('Editor');
@ -281,7 +287,7 @@ describe('Acceptance: Team', function () {
// validation message is displayed
expect(
find('.fullscreen-modal .error .response').text().trim(),
find('.fullscreen-modal .error .response').textContent.trim(),
'inviting existing user error'
).to.equal('A user with that email address already exists.');
@ -291,7 +297,7 @@ describe('Acceptance: Team', function () {
// validation message is displayed
expect(
find('.fullscreen-modal .error .response').text().trim(),
find('.fullscreen-modal .error .response').textContent.trim(),
'inviting invited user error'
).to.equal('A user with that email address was already invited.');
@ -301,7 +307,7 @@ describe('Acceptance: Team', function () {
// validation message is displayed
expect(
find('.fullscreen-modal .error .response').text().trim(),
find('.fullscreen-modal .error .response').textContent.trim(),
'inviting invalid email error'
).to.equal('Invalid Email.');
@ -311,19 +317,19 @@ describe('Acceptance: Team', function () {
// number of invites decreases
expect(
find('[data-test-invite-id]').length,
findAll('[data-test-invite-id]').length,
'number of invites after revoke'
).to.equal(1);
// notification is displayed
expect(
find('.gh-notification').text().trim(),
find('.gh-notification').textContent.trim(),
'notifications contain revoke'
).to.match(/Invitation revoked\. \(invite2@example\.com\)/);
// correct invite is removed
expect(
find('[data-test-invite-id] [data-test-email]').text().trim(),
find('[data-test-invite-id] [data-test-email]').textContent.trim(),
'displayed email of remaining invite'
).to.equal('invite1@example.com');
@ -334,7 +340,7 @@ describe('Acceptance: Team', function () {
// new invite should be last in the list
expect(
find('[data-test-invite-id]:last [data-test-email]').text().trim(),
find('[data-test-invite-id]:last-of-type [data-test-email]').textContent.trim(),
'last invite email in list'
).to.equal('invite3@example.com');
@ -343,13 +349,13 @@ describe('Acceptance: Team', function () {
// notification is displayed
expect(
find('.gh-notification').text().trim(),
find('.gh-notification').textContent.trim(),
'notifications contain resend'
).to.match(/Invitation resent! \(invite1@example\.com\)/);
// first invite is still at the top
expect(
find('[data-test-invite-id]:first-of-type [data-test-email]').text().trim(),
find('[data-test-invite-id]:first-of-type [data-test-email]').textContent.trim(),
'first invite email in list'
).to.equal('invite1@example.com');
@ -359,13 +365,13 @@ describe('Acceptance: Team', function () {
// number of invites decreases
expect(
find('[data-test-invite-id]').length,
findAll('[data-test-invite-id]').length,
'number of invites after resend/revoke'
).to.equal(1);
// notification is displayed
expect(
find('.gh-notification').text().trim(),
find('.gh-notification').textContent.trim(),
'notifications contain revoke after resend/revoke'
).to.match(/Invitation revoked\. \(invite1@example\.com\)/);
});
@ -395,7 +401,7 @@ describe('Acceptance: Team', function () {
// no suspended users
expect(
find('[data-test-suspended-users] [data-test-user-id]').length
findAll('[data-test-suspended-users] [data-test-user-id]').length
).to.equal(0);
await click(`[data-test-user-id="${suspendedUser.id}"]`);
@ -407,9 +413,9 @@ describe('Acceptance: Team', function () {
});
it('can delete users', async function () {
let user1 = server.create('user');
let user2 = server.create('user');
let post = server.create('post', {authors: [user2]});
let user1 = this.server.create('user');
let user2 = this.server.create('user');
let post = this.server.create('post', {authors: [user2]});
// we don't have a full many-to-many relationship in mirage so we
// need to add the inverse manually
@ -422,20 +428,20 @@ describe('Acceptance: Team', function () {
// user deletion displays modal
await click('button.delete');
expect(
find('.fullscreen-modal .modal-content:contains("delete this user")').length,
findAll('[data-test-modal="delete-user"]').length,
'user deletion modal displayed after button click'
).to.equal(1);
// user has no posts so no warning about post deletion
expect(
find('.fullscreen-modal .modal-content:contains("is the author of")').length,
findAll('[data-test-text="user-post-count"]').length,
'deleting user with no posts has no post count'
).to.equal(0);
// cancelling user deletion closes modal
await click('.fullscreen-modal button:contains("Cancel")');
await click('[data-test-button="cancel-delete-user"]');
expect(
find('.fullscreen-modal').length === 0,
findAll('[data-test-modal]').length === 0,
'delete user modal is closed when cancelling'
).to.be.true;
@ -446,17 +452,17 @@ describe('Acceptance: Team', function () {
await click('button.delete');
// user has posts so should warn about post deletion
expect(
find('.fullscreen-modal .modal-content:contains("1 post created by this user")').length,
find('[data-test-text="user-post-count"]').textContent,
'deleting user with posts has post count'
).to.equal(1);
).to.have.string('1 post');
await click('.fullscreen-modal button:contains("Delete")');
await click('[data-test-button="confirm-delete-user"]');
// redirected to team page
expect(currentURL()).to.equal('/team');
// deleted user is not in list
expect(
find(`[data-test-user-id="${user2.id}"]`).length,
findAll(`[data-test-user-id="${user2.id}"]`).length,
'deleted user is not in user list after deletion'
).to.equal(0);
});
@ -465,7 +471,7 @@ describe('Acceptance: Team', function () {
let user, newLocation, originalReplaceState;
beforeEach(function () {
user = server.create('user', {
user = this.server.create('user', {
slug: 'test-1',
name: 'Test User',
facebook: 'test',
@ -488,36 +494,36 @@ describe('Acceptance: Team', function () {
await visit('/team/test-1');
expect(currentURL(), 'currentURL').to.equal('/team/test-1');
expect(find('[data-test-name-input]').val(), 'current user name').to.equal('Test User');
expect(find('[data-test-name-input]').value, 'current user name').to.equal('Test User');
expect(find('[data-test-save-button]').text().trim(), 'save button text').to.equal('Save');
expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Save');
// test empty user name
await fillIn('[data-test-name-input]', '');
await triggerEvent('[data-test-name-input]', 'blur');
await blur('[data-test-name-input]');
expect(find('.user-details-bottom .first-form-group').hasClass('error'), 'username input is in error state with blank input').to.be.true;
expect(find('.user-details-bottom .first-form-group').classList.contains('error'), 'username input is in error state with blank input').to.be.true;
// test too long user name
await fillIn('[data-test-name-input]', new Array(195).join('a'));
await triggerEvent('[data-test-name-input]', 'blur');
await blur('[data-test-name-input]');
expect(find('.user-details-bottom .first-form-group').hasClass('error'), 'username input is in error state with too long input').to.be.true;
expect(find('.user-details-bottom .first-form-group').classList.contains('error'), 'username input is in error state with too long input').to.be.true;
// reset name field
await fillIn('[data-test-name-input]', 'Test User');
expect(find('[data-test-slug-input]').val(), 'slug value is default').to.equal('test-1');
expect(find('[data-test-slug-input]').value, 'slug value is default').to.equal('test-1');
await fillIn('[data-test-slug-input]', '');
await triggerEvent('[data-test-slug-input]', 'blur');
await blur('[data-test-slug-input]');
expect(find('[data-test-slug-input]').val(), 'slug value is reset to original upon empty string').to.equal('test-1');
expect(find('[data-test-slug-input]').value, 'slug value is reset to original upon empty string').to.equal('test-1');
// Save changes
await click('[data-test-save-button]');
expect(find('[data-test-save-button]').text().trim(), 'save button text').to.equal('Saved');
expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Saved');
// CMD-S shortcut works
await fillIn('[data-test-slug-input]', 'Test User');
@ -529,7 +535,7 @@ describe('Acceptance: Team', function () {
// we've already saved in this test so there's no on-screen indication
// that we've had another save, check the request was fired instead
let [lastRequest] = server.pretender.handledRequests.slice(-1);
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
let params = JSON.parse(lastRequest.requestBody);
expect(params.users[0].name).to.equal('Test User');
@ -538,43 +544,49 @@ describe('Acceptance: Team', function () {
expect(newLocation).to.equal('Test User');
await fillIn('[data-test-slug-input]', 'white space');
await triggerEvent('[data-test-slug-input]', 'blur');
await blur('[data-test-slug-input]');
expect(find('[data-test-slug-input]').val(), 'slug value is correctly dasherized').to.equal('white-space');
expect(find('[data-test-slug-input]').value, 'slug value is correctly dasherized').to.equal('white-space');
await fillIn('[data-test-email-input]', 'thisisnotanemail');
await triggerEvent('[data-test-email-input]', 'blur');
await blur('[data-test-email-input]');
expect(find('.user-details-bottom .form-group:nth-of-type(3)').hasClass('error'), 'email input should be in error state with invalid email').to.be.true;
expect(find('.user-details-bottom .form-group:nth-of-type(3)').classList.contains('error'), 'email input should be in error state with invalid email').to.be.true;
await fillIn('[data-test-email-input]', 'test@example.com');
await fillIn('[data-test-location-input]', new Array(160).join('a'));
await triggerEvent('[data-test-location-input]', 'blur');
await blur('[data-test-location-input]');
expect(find('[data-test-location-input]').closest('.form-group').hasClass('error'), 'location input should be in error state').to.be.true;
expect(
find('[data-test-location-input]').closest('.form-group'),
'location input should be in error state'
).to.have.class('error');
await fillIn('[data-test-location-input]', '');
await fillIn('[data-test-website-input]', 'thisisntawebsite');
await triggerEvent('[data-test-website-input]', 'blur');
await blur('[data-test-website-input]');
expect(find('[data-test-website-input]').closest('.form-group').hasClass('error'), 'website input should be in error state').to.be.true;
expect(
find('[data-test-website-input]').closest('.form-group'),
'website input should be in error state'
).to.have.class('error');
let testSocialInput = async function (type, input, expectedValue, expectedError = '') {
await fillIn(`[data-test-${type}-input]`, input);
await triggerEvent(`[data-test-${type}-input]`, 'blur');
await blur(`[data-test-${type}-input]`);
expect(
find(`[data-test-${type}-input]`).val(),
find(`[data-test-${type}-input]`).value,
`${type} value for ${input}`
).to.equal(expectedValue);
expect(
find(`[data-test-${type}-error]`).text().trim(),
find(`[data-test-error="user-${type}"]`).textContent.trim(),
`${type} validation response for ${input}`
).to.equal(expectedError);
expect(
find(`[data-test-${type}-input]`).closest('.form-group').hasClass('error'),
find(`[data-test-error="user-${type}"]`).closest('.form-group').classList.contains('error'),
`${type} input should be in error state with '${input}'`
).to.equal(!!expectedError);
};
@ -585,15 +597,15 @@ describe('Acceptance: Team', function () {
// Testing Facebook input
// displays initial value
expect(find('[data-test-facebook-input]').val(), 'initial facebook value')
expect(find('[data-test-facebook-input]').value, 'initial facebook value')
.to.equal('https://www.facebook.com/test');
await triggerEvent('[data-test-facebook-input]', 'focus');
await triggerEvent('[data-test-facebook-input]', 'blur');
await focus('[data-test-facebook-input]');
await blur('[data-test-facebook-input]');
// regression test: we still have a value after the input is
// focused and then blurred without any changes
expect(find('[data-test-facebook-input]').val(), 'facebook value after blur with no change')
expect(find('[data-test-facebook-input]').value, 'facebook value after blur with no change')
.to.equal('https://www.facebook.com/test');
await testFacebookValidation(
@ -637,15 +649,15 @@ describe('Acceptance: Team', function () {
// Testing Twitter input
// loads fixtures and performs transform
expect(find('[data-test-twitter-input]').val(), 'initial twitter value')
expect(find('[data-test-twitter-input]').value, 'initial twitter value')
.to.equal('https://twitter.com/test');
await triggerEvent('[data-test-twitter-input]', 'focus');
await triggerEvent('[data-test-twitter-input]', 'blur');
await focus('[data-test-twitter-input]');
await blur('[data-test-twitter-input]');
// regression test: we still have a value after the input is
// focused and then blurred without any changes
expect(find('[data-test-twitter-input]').val(), 'twitter value after blur with no change')
expect(find('[data-test-twitter-input]').value, 'twitter value after blur with no change')
.to.equal('https://twitter.com/test');
await testTwitterValidation(
@ -674,9 +686,12 @@ describe('Acceptance: Team', function () {
await fillIn('[data-test-website-input]', '');
await fillIn('[data-test-bio-input]', new Array(210).join('a'));
await triggerEvent('[data-test-bio-input]', 'blur');
await blur('[data-test-bio-input]');
expect(find('[data-test-bio-input]').closest('.form-group').hasClass('error'), 'bio input should be in error state').to.be.true;
expect(
find('[data-test-bio-input]').closest('.form-group'),
'bio input should be in error state'
).to.have.class('error');
// password reset ------
@ -684,47 +699,47 @@ describe('Acceptance: Team', function () {
await click('[data-test-save-pw-button]');
expect(
find('[data-test-new-pass-input]').closest('.form-group').hasClass('error'),
find('[data-test-new-pass-input]').closest('.form-group'),
'new password has error class when blank'
).to.be.true;
).to.have.class('error');
expect(
find('[data-test-new-pass-input]').siblings('.response').text(),
find('[data-test-error="user-new-pass"]').textContent,
'new password error when blank'
).to.match(/can't be blank/);
).to.have.string('can\'t be blank');
// validates too short password (< 10 characters)
await fillIn('[data-test-new-pass-input]', 'notlong');
await fillIn('[data-test-ne2-pass-input]', 'notlong');
// enter key triggers action
await keyEvent('[data-test-new-pass-input]', 'keyup', 13);
await triggerKeyEvent('[data-test-new-pass-input]', 'keyup', 13);
expect(
find('[data-test-new-pass-input]').closest('.form-group').hasClass('error'),
find('[data-test-new-pass-input]').closest('.form-group'),
'new password has error class when password too short'
).to.be.true;
).to.have.class('error');
expect(
find('[data-test-new-pass-input]').siblings('.response').text(),
'confirm password error when it\'s too short'
).to.match(/at least 10 characters long/);
find('[data-test-error="user-new-pass"]').textContent,
'new password error when it\'s too short'
).to.have.string('at least 10 characters long');
// validates unsafe password
await fillIn('#user-password-new', 'ghostisawesome');
await fillIn('#user-new-password-verification', 'ghostisawesome');
await fillIn('[data-test-ne2-pass-input]', 'ghostisawesome');
// enter key triggers action
await keyEvent('#user-password-new', 'keyup', 13);
await triggerKeyEvent('#user-password-new', 'keyup', 13);
expect(
find('#user-password-new').closest('.form-group').hasClass('error'),
find('#user-password-new').closest('.form-group'),
'new password has error class when password is insecure'
).to.be.true;
).to.have.class('error');
expect(
find('#user-password-new').siblings('.response').text(),
'confirm password error when it\'s insecure'
find('[data-test-error="user-new-pass"]').textContent,
'new password error when it\'s insecure'
).to.match(/you cannot use an insecure password/);
// typing in inputs clears validation
@ -732,29 +747,29 @@ describe('Acceptance: Team', function () {
await triggerEvent('[data-test-new-pass-input]', 'input');
expect(
find('[data-test-new-pass-input]').closest('.form-group').hasClass('error'),
find('[data-test-new-pass-input]').closest('.form-group'),
'password validation is visible after typing'
).to.be.false;
).to.not.have.class('error');
// enter key triggers action
await keyEvent('[data-test-new-pass-input]', 'keyup', 13);
await triggerKeyEvent('[data-test-new-pass-input]', 'keyup', 13);
expect(
find('[data-test-ne2-pass-input]').closest('.form-group').hasClass('error'),
find('[data-test-ne2-pass-input]').closest('.form-group'),
'confirm password has error class when it doesn\'t match'
).to.be.true;
).to.have.class('error');
expect(
find('[data-test-ne2-pass-input]').siblings('.response').text(),
find('[data-test-error="user-ne2-pass"]').textContent,
'confirm password error when it doesn\'t match'
).to.match(/do not match/);
).to.have.string('do not match');
// submits with correct details
await fillIn('[data-test-ne2-pass-input]', 'thisissupersafe');
await click('[data-test-save-pw-button]');
// hits the endpoint
let [newRequest] = server.pretender.handledRequests.slice(-1);
let [newRequest] = this.server.pretender.handledRequests.slice(-1);
params = JSON.parse(newRequest.requestBody);
expect(newRequest.url, 'password request URL')
@ -767,18 +782,18 @@ describe('Acceptance: Team', function () {
// clears the fields
expect(
find('[data-test-new-pass-input]').val(),
find('[data-test-new-pass-input]').value,
'password field after submit'
).to.be.empty;
expect(
find('[data-test-ne2-pass-input]').val(),
find('[data-test-ne2-pass-input]').value,
'password verification field after submit'
).to.be.empty;
// displays a notification
expect(
find('.gh-notifications .gh-notification').length,
findAll('.gh-notifications .gh-notification').length,
'password saved notification is displayed'
).to.equal(1);
});
@ -789,21 +804,21 @@ describe('Acceptance: Team', function () {
expect(currentURL(), 'currentURL').to.equal('/team/test-1');
await fillIn('[data-test-slug-input]', 'another slug');
await triggerEvent('[data-test-slug-input]', 'blur');
await blur('[data-test-slug-input]');
expect(find('[data-test-slug-input]').val()).to.be.equal('another-slug');
expect(find('[data-test-slug-input]').value).to.be.equal('another-slug');
await fillIn('[data-test-facebook-input]', 'testuser');
await triggerEvent('[data-test-facebook-input]', 'blur');
await blur('[data-test-facebook-input]');
expect(find('[data-test-facebook-input]').val()).to.be.equal('https://www.facebook.com/testuser');
expect(find('[data-test-facebook-input]').value).to.be.equal('https://www.facebook.com/testuser');
await visit('/settings/team');
expect(find('.fullscreen-modal').length, 'modal exists').to.equal(1);
expect(findAll('[data-test-modal]').length, 'modal exists').to.equal(1);
// Leave without saving
await (click('.fullscreen-modal [data-test-leave-button]'), 'leave without saving');
await click('.fullscreen-modal [data-test-leave-button]');
expect(currentURL(), 'currentURL').to.equal('/settings/team');
@ -812,8 +827,8 @@ describe('Acceptance: Team', function () {
expect(currentURL(), 'currentURL').to.equal('/team/test-1');
// settings were not saved
expect(find('[data-test-slug-input]').val()).to.be.equal('test-1');
expect(find('[data-test-facebook-input]').val()).to.be.equal('https://www.facebook.com/test');
expect(find('[data-test-slug-input]').value).to.be.equal('test-1');
expect(find('[data-test-facebook-input]').value).to.be.equal('https://www.facebook.com/test');
});
});
@ -826,39 +841,39 @@ describe('Acceptance: Team', function () {
// old password has error
expect(
find('[data-test-old-pass-input]').closest('.form-group').hasClass('error'),
find('[data-test-old-pass-input]').closest('.form-group'),
'old password has error class when blank'
).to.be.true;
).to.have.class('error');
expect(
find('[data-test-old-pass-input]').siblings('.response').text(),
find('[data-test-error="user-old-pass"]').textContent,
'old password error when blank'
).to.match(/is required/);
).to.have.string('is required');
// new password has error
expect(
find('[data-test-new-pass-input]').closest('.form-group').hasClass('error'),
find('[data-test-new-pass-input]').closest('.form-group'),
'new password has error class when blank'
).to.be.true;
).to.have.class('error');
expect(
find('[data-test-new-pass-input]').siblings('.response').text(),
find('[data-test-error="user-new-pass"]').textContent,
'new password error when blank'
).to.match(/can't be blank/);
).to.have.string('can\'t be blank');
// validation is cleared when typing
await fillIn('[data-test-old-pass-input]', 'password');
await triggerEvent('[data-test-old-pass-input]', 'input');
expect(
find('[data-test-old-pass-input]').closest('.form-group').hasClass('error'),
find('[data-test-old-pass-input]').closest('.form-group'),
'old password validation is in error state after typing'
).to.be.false;
).to.not.have.class('error');
});
});
it('redirects to 404 when user does not exist', async function () {
server.get('/users/slug/unknown/', function () {
this.server.get('/users/slug/unknown/', function () {
return new Response(404, {'Content-Type': 'application/json'}, {errors: [{message: 'User not found.', errorType: 'NotFoundError'}]});
});
@ -867,7 +882,7 @@ describe('Acceptance: Team', function () {
await visit('/team/unknown');
errorReset();
expect(currentPath()).to.equal('error404');
expect(currentRouteName()).to.equal('error404');
expect(currentURL()).to.equal('/team/unknown');
});
});
@ -875,12 +890,12 @@ describe('Acceptance: Team', function () {
describe('when logged in as author', function () {
let adminRole, authorRole;
beforeEach(function () {
adminRole = server.create('role', {name: 'Administrator'});
authorRole = server.create('role', {name: 'Author'});
server.create('user', {roles: [authorRole]});
beforeEach(async function () {
adminRole = this.server.create('role', {name: 'Administrator'});
authorRole = this.server.create('role', {name: 'Author'});
this.server.create('user', {roles: [authorRole]});
server.get('/invites/', function () {
this.server.get('/invites/', function () {
return new Response(403, {}, {
errors: [{
errorType: 'NoPermissionError',
@ -889,20 +904,20 @@ describe('Acceptance: Team', function () {
});
});
return authenticateSession(application);
return await authenticateSession();
});
it('can access the team page', async function () {
server.create('user', {roles: [adminRole]});
server.create('invite', {role: authorRole});
this.server.create('user', {roles: [adminRole]});
this.server.create('invite', {role: authorRole});
errorOverride();
await visit('/team');
errorReset();
expect(currentPath()).to.equal('team.index');
expect(find('.gh-alert').length).to.equal(0);
expect(currentRouteName()).to.equal('team.index');
expect(findAll('.gh-alert').length).to.equal(0);
});
});
});

View file

@ -1,15 +0,0 @@
/* global key */
import {run} from '@ember/runloop';
export default function destroyApp(application) {
// this is required to fix "second Pretender instance" warnings
if (server) {
server.shutdown();
}
// extra check to ensure we don't have references hanging around via key
// bindings on supposedly destroyed objects
key.deleteScope('default');
run(application, 'destroy');
}

View file

@ -1,6 +1,4 @@
/* global Blob */
import $ from 'jquery';
import {registerAsyncHelper} from '@ember/test';
import {triggerEvent} from '@ember/test-helpers';
export function createFile(content = ['test'], options = {}) {
let {
@ -14,22 +12,12 @@ export function createFile(content = ['test'], options = {}) {
return file;
}
export function fileUpload($element, content, options) {
export function fileUpload(target, content, options) {
let file = createFile(content, options);
// eslint-disable-next-line new-cap
let event = $.Event('change', {
testingFiles: [file]
});
$element.trigger(event);
}
export default registerAsyncHelper('fileUpload', function (app, selector, content, options) {
let file = createFile(content, options);
// TODO: replace `[file]` with `{files: [file]}` after upgrading ember-test-helpers
return triggerEvent(
selector,
target,
'change',
{testingFiles: [file]}
[file]
);
});
}

13
tests/helpers/find.js Normal file
View file

@ -0,0 +1,13 @@
import {findAll} from '@ember/test-helpers';
export function elementHasText(element, text) {
return RegExp(text).test(element.textContent);
}
export function findWithText(selector, text) {
return Array.from(findAll(selector)).find(element => elementHasText(element, text));
}
export function findAllWithText(selector, text) {
return Array.from(findAll(selector)).filter(element => elementHasText(element, text));
}

View file

@ -1,22 +0,0 @@
import Application from '../../app';
import config from '../../config/environment';
import fileUpload from './file-upload'; // eslint-disable-line
import registerPowerDatepickerHelpers from '../../tests/helpers/ember-power-datepicker';
import registerPowerSelectHelpers from 'ember-power-select/test-support/helpers';
import {assign} from '@ember/polyfills';
import {run} from '@ember/runloop';
registerPowerSelectHelpers();
registerPowerDatepickerHelpers();
export default function startApp(attrs) {
let attributes = assign({}, config.APP);
attributes = assign(attributes, attrs); // use defaults, but you can override;
return run(() => {
let application = Application.create(attributes);
application.setupForTesting();
application.injectTestHelpers();
return application;
});
}

16
tests/helpers/visit.js Normal file
View file

@ -0,0 +1,16 @@
// TODO: remove once bug is fixed in Ember
// see https://github.com/emberjs/ember-test-helpers/issues/332
import {visit as _visit, settled} from '@ember/test-helpers';
export async function visit(url) {
try {
await _visit(url);
} catch (e) {
if (e.message !== 'TransitionAborted') {
throw e;
}
}
await settled();
}

View file

@ -1,40 +1,38 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {render} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-alert', function () {
setupComponentTest('gh-alert', {
integration: true
});
setupRenderingTest();
it('renders', function () {
it('renders', async function () {
this.set('message', {message: 'Test message', type: 'success'});
this.render(hbs`{{gh-alert message=message}}`);
await render(hbs`{{gh-alert message=message}}`);
expect(this.$('article.gh-alert')).to.have.length(1);
let $alert = this.$('.gh-alert');
expect($alert.text()).to.match(/Test message/);
let alert = this.element.querySelector('article.gh-alert');
expect(alert).to.exist;
expect(alert).to.contain.text('Test message');
});
it('maps message types to CSS classes', function () {
it('maps message types to CSS classes', async function () {
this.set('message', {message: 'Test message', type: 'success'});
this.render(hbs`{{gh-alert message=message}}`);
let $alert = this.$('.gh-alert');
await render(hbs`{{gh-alert message=message}}`);
let alert = this.element.querySelector('article.gh-alert');
this.set('message.type', 'success');
expect($alert.hasClass('gh-alert-green'), 'success class isn\'t green').to.be.true;
expect(alert, 'success class is green').to.have.class('gh-alert-green');
this.set('message.type', 'error');
expect($alert.hasClass('gh-alert-red'), 'success class isn\'t red').to.be.true;
expect(alert, 'error class is red').to.have.class('gh-alert-red');
this.set('message.type', 'warn');
expect($alert.hasClass('gh-alert-blue'), 'success class isn\'t yellow').to.be.true;
expect(alert, 'warn class is yellow').to.have.class('gh-alert-blue');
this.set('message.type', 'info');
expect($alert.hasClass('gh-alert-blue'), 'success class isn\'t blue').to.be.true;
expect(alert, 'info class is blue').to.have.class('gh-alert-blue');
});
});

View file

@ -3,48 +3,53 @@ import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {A as emberA} from '@ember/array';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {find, findAll, render, settled} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
let notificationsStub = Service.extend({
alerts: emberA()
});
describe('Integration: Component: gh-alerts', function () {
setupComponentTest('gh-alerts', {
integration: true
});
setupRenderingTest();
beforeEach(function () {
this.register('service:notifications', notificationsStub);
this.inject.service('notifications', {as: 'notifications'});
this.owner.register('service:notifications', notificationsStub);
let notifications = this.owner.lookup('service:notifications');
this.set('notifications.alerts', [
notifications.set('alerts', [
{message: 'First', type: 'error'},
{message: 'Second', type: 'warn'}
]);
});
it('renders', function () {
this.render(hbs`{{gh-alerts}}`);
expect(this.$('.gh-alerts').length).to.equal(1);
expect(this.$('.gh-alerts').children().length).to.equal(2);
it('renders', async function () {
let notifications = this.owner.lookup('service:notifications');
this.set('notifications.alerts', emberA());
expect(this.$('.gh-alerts').children().length).to.equal(0);
await render(hbs`{{gh-alerts}}`);
expect(findAll('.gh-alerts').length).to.equal(1);
expect(find('.gh-alerts').children.length).to.equal(2);
notifications.set('alerts', emberA());
await settled();
expect(find('.gh-alerts').children.length).to.equal(0);
});
it('triggers "notify" action when message count changes', function () {
it('triggers "notify" action when message count changes', async function () {
let notifications = this.owner.lookup('service:notifications');
let expectedCount = 0;
// test double for notify action
this.set('notify', count => expect(count).to.equal(expectedCount));
this.render(hbs`{{gh-alerts notify=(action notify)}}`);
await render(hbs`{{gh-alerts notify=(action notify)}}`);
expectedCount = 3;
this.get('notifications.alerts').pushObject({message: 'Third', type: 'success'});
notifications.alerts.pushObject({message: 'Third', type: 'success'});
await settled();
expectedCount = 0;
this.set('notifications.alerts', emberA());
notifications.set('alerts', emberA());
await settled();
});
});

View file

@ -1,21 +1,17 @@
import $ from 'jquery';
import hbs from 'htmlbars-inline-precompile';
import {clickTrigger} from '../../helpers/ember-basic-dropdown';
import {clickTrigger} from 'ember-basic-dropdown/test-support/helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {find} from 'ember-native-dom-helpers';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {find, render, settled} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-basic-dropdown', function () {
setupComponentTest('gh-basic-dropdown', {
integration: true
});
setupRenderingTest();
it('closes when dropdown service fires close event', function () {
let dropdownService = this.container.lookup('service:dropdown');
it('closes when dropdown service fires close event', async function () {
let dropdownService = this.owner.lookup('service:dropdown');
this.render(hbs`
await render(hbs`
{{#gh-basic-dropdown as |dropdown|}}
<button class="ember-basic-dropdown-trigger" onclick={{dropdown.actions.toggle}}></button>
{{#if dropdown.isOpen}}
@ -24,13 +20,12 @@ describe('Integration: Component: gh-basic-dropdown', function () {
{{/gh-basic-dropdown}}
`);
clickTrigger();
expect($(find('#dropdown-is-opened'))).to.exist;
await clickTrigger();
expect(find('#dropdown-is-opened')).to.exist;
run(() => {
dropdownService.closeDropdowns();
});
await settled();
expect($(find('#dropdown-is-opened'))).to.not.exist;
expect(find('#dropdown-is-opened')).to.not.exist;
});
});

View file

@ -1,91 +1,81 @@
import $ from 'jquery';
import hbs from 'htmlbars-inline-precompile';
import wait from 'ember-test-helpers/wait';
import {click, find, triggerEvent} from 'ember-native-dom-helpers';
import {click, find, render, settled, triggerEvent} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
// NOTE: If the browser window is not focused/visible CodeMirror (or Chrome?) will
// take longer to respond to/fire events so it's possible that some of these tests
// will take 1-3 seconds
describe('Integration: Component: gh-cm-editor', function () {
setupComponentTest('gh-cm-editor', {
integration: true
});
setupRenderingTest();
it('handles change event', function () {
it('handles change event', async function () {
this.set('text', '');
this.render(hbs`{{gh-cm-editor text class="gh-input" update=(action (mut text))}}`);
await render(hbs`{{gh-cm-editor text class="gh-input" update=(action (mut text))}}`);
// access CodeMirror directly as it doesn't pick up changes to the textarea
let cm = find('.gh-input .CodeMirror').CodeMirror;
cm.setValue('Testing');
return wait().then(() => {
await settled();
expect(this.get('text'), 'text value after CM editor change')
.to.equal('Testing');
});
});
it('handles focus event', function (done) {
it('handles focus event', async function () {
// CodeMirror's events are triggered outside of anything we can watch for
// in the tests so let's run the class check when we know the event has
// been fired and timeout if it's not fired as we expect
let onFocus = () => {
let onFocus = async () => {
// wait for runloop to finish so that the new class has been rendered
wait().then(() => {
expect($(find('.gh-input')).hasClass('focus'), 'has focused class on first render with autofocus')
.to.be.true;
done();
});
await settled();
expect(find('.gh-input'), 'has focused class on first render with autofocus')
.to.have.class('focus');
};
this.set('onFocus', onFocus);
this.set('text', '');
this.render(hbs`{{gh-cm-editor text class="gh-input" update=(action (mut text)) focus-in=(action onFocus)}}`);
await render(hbs`{{gh-cm-editor text class="gh-input" update=(action (mut text)) focus-in=(action onFocus)}}`);
// CodeMirror polls the input for changes every 100ms
click('textarea');
triggerEvent('textarea', 'focus');
await click('textarea');
await triggerEvent('textarea', 'focus');
});
it('handles blur event', async function () {
this.set('text', '');
this.render(hbs`{{gh-cm-editor text class="gh-input" update=(action (mut text))}}`);
await render(hbs`{{gh-cm-editor text class="gh-input" update=(action (mut text))}}`);
expect($(find('.gh-input')).hasClass('focus')).to.be.false;
expect(find('.gh-input')).to.not.have.class('focus');
await click('textarea');
await triggerEvent('textarea', 'focus');
expect($(find('.gh-input')).hasClass('focus')).to.be.true;
expect(find('.gh-input')).to.have.class('focus');
await triggerEvent('textarea', 'blur');
expect($(find('.gh-input')).hasClass('focus')).to.be.false;
expect(find('.gh-input')).to.not.have.class('focus');
});
it('can autofocus', function (done) {
it('can autofocus', async function () {
// CodeMirror's events are triggered outside of anything we can watch for
// in the tests so let's run the class check when we know the event has
// been fired and timeout if it's not fired as we expect
let onFocus = () => {
let onFocus = async () => {
// wait for runloop to finish so that the new class has been rendered
wait().then(() => {
expect(this.$('.gh-input').hasClass('focus'), 'has focused class on first render with autofocus')
await settled();
expect(find('.gh-input').classList.contains('focus'), 'has focused class on first render with autofocus')
.to.be.true;
done();
});
};
this.set('onFocus', onFocus);
this.set('text', '');
this.render(hbs`{{gh-cm-editor text class="gh-input" update=(action (mut text)) autofocus=true focus-in=(action onFocus)}}`);
await render(hbs`{{gh-cm-editor text class="gh-input" update=(action (mut text)) autofocus=true focus-in=(action onFocus)}}`);
});
});

View file

@ -1,24 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-date-time-picker', function () {
setupComponentTest('gh-date-time-picker', {
integration: true
});
it.skip('renders', function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#gh-date-time-picker}}
// template content
// {{/gh-date-time-picker}}
// `);
this.render(hbs`{{gh-date-time-picker}}`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -1,14 +1,12 @@
import Pretender from 'pretender';
import hbs from 'htmlbars-inline-precompile';
import wait from 'ember-test-helpers/wait';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {render} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-download-count', function () {
setupComponentTest('gh-download-count', {
integration: true
});
setupRenderingTest();
let server;
@ -23,23 +21,19 @@ describe('Integration: Component: gh-download-count', function () {
server.shutdown();
});
it('hits count endpoint and renders', function () {
this.render(hbs`{{gh-download-count}}`);
it('hits count endpoint and renders', async function () {
await render(hbs`{{gh-download-count}}`);
return wait().then(() => {
expect(this.$().text().trim()).to.equal('42');
});
expect(this.element).to.have.trimmed.text('42');
});
it('renders with a block', function () {
this.render(hbs`
it('renders with a block', async function () {
await render(hbs`
{{#gh-download-count as |count|}}
{{count}} downloads
{{/gh-download-count}}
`);
return wait().then(() => {
expect(this.$().text().trim()).to.equal('42 downloads');
});
expect(this.element).to.have.trimmed.text('42 downloads');
});
});

View file

@ -1,24 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-editor-post-status', function () {
setupComponentTest('gh-editor-post-status', {
integration: true
});
it('renders', function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#gh-editor-post-status}}
// template content
// {{/gh-editor-post-status}}
// `);
this.render(hbs`{{gh-editor-post-status}}`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -1,52 +1,44 @@
import Service from '@ember/service';
import hbs from 'htmlbars-inline-precompile';
import {click, find, render} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
const featureStub = Service.extend({
testFlag: true
});
describe('Integration: Component: gh-feature-flag', function () {
setupComponentTest('gh-feature-flag', {
integration: true
});
setupRenderingTest();
beforeEach(function () {
this.register('service:feature', featureStub);
this.inject.service('feature', {as: 'feature'});
this.owner.register('service:feature', featureStub);
});
it('renders properties correctly', function () {
this.render(hbs`{{gh-feature-flag "testFlag"}}`);
expect(this.$()).to.have.length(1);
expect(this.$('label').attr('for')).to.equal(this.$('input[type="checkbox"]').attr('id'));
it('renders properties correctly', async function () {
await render(hbs`{{gh-feature-flag "testFlag"}}`);
expect(find('label').getAttribute('for')).to.equal(find('input[type="checkbox"]').id);
});
it('renders correctly when flag is set to true', function () {
this.render(hbs`{{gh-feature-flag "testFlag"}}`);
expect(this.$()).to.have.length(1);
expect(this.$('label input[type="checkbox"]').prop('checked')).to.be.true;
it('renders correctly when flag is set to true', async function () {
await render(hbs`{{gh-feature-flag "testFlag"}}`);
expect(find('label input[type="checkbox"]').checked).to.be.true;
});
it('renders correctly when flag is set to false', function () {
this.set('feature.testFlag', false);
it('renders correctly when flag is set to false', async function () {
let feature = this.owner.lookup('service:feature');
feature.set('testFlag', false);
this.render(hbs`{{gh-feature-flag "testFlag"}}`);
expect(this.$()).to.have.length(1);
expect(this.$('label input[type="checkbox"]').prop('checked')).to.be.false;
await render(hbs`{{gh-feature-flag "testFlag"}}`);
expect(find('label input[type="checkbox"]').checked).to.be.false;
});
it('updates to reflect changes in flag property', function () {
this.render(hbs`{{gh-feature-flag "testFlag"}}`);
expect(this.$()).to.have.length(1);
it('updates to reflect changes in flag property', async function () {
await render(hbs`{{gh-feature-flag "testFlag"}}`);
expect(find('label input[type="checkbox"]').checked).to.be.true;
expect(this.$('label input[type="checkbox"]').prop('checked')).to.be.true;
this.$('label').click();
expect(this.$('label input[type="checkbox"]').prop('checked')).to.be.false;
await click('label');
expect(find('label input[type="checkbox"]').checked).to.be.false;
});
});

View file

@ -3,13 +3,13 @@ import Pretender from 'pretender';
import Service from '@ember/service';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import wait from 'ember-test-helpers/wait';
import {UnsupportedMediaTypeError} from 'ghost-admin/services/ajax';
import {click, find, findAll, render, settled, triggerEvent} from '@ember/test-helpers';
import {createFile, fileUpload} from '../../helpers/file-upload';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
const notificationsStub = Service.extend({
showAPIError() {
@ -35,9 +35,7 @@ const stubFailedUpload = function (server, code, error, delay = 0) {
};
describe('Integration: Component: gh-file-uploader', function () {
setupComponentTest('gh-file-uploader', {
integration: true
});
setupRenderingTest();
let server;
@ -45,281 +43,234 @@ describe('Integration: Component: gh-file-uploader', function () {
server = new Pretender();
this.set('uploadUrl', '/ghost/api/v2/admin/uploads/');
this.register('service:notifications', notificationsStub);
this.inject.service('notifications', {as: 'notifications'});
this.owner.register('service:notifications', notificationsStub);
});
afterEach(function () {
server.shutdown();
});
it('renders', function () {
this.render(hbs`{{gh-file-uploader}}`);
it('renders', async function () {
await render(hbs`{{gh-file-uploader}}`);
expect(this.$('label').text().trim(), 'default label')
expect(find('label').textContent.trim(), 'default label')
.to.equal('Select or drag-and-drop a file');
});
it('allows file input "accept" attribute to be changed', function () {
this.render(hbs`{{gh-file-uploader}}`);
it('allows file input "accept" attribute to be changed', async function () {
await render(hbs`{{gh-file-uploader}}`);
expect(
this.$('input[type="file"]').attr('accept'),
find('input[type="file"]').getAttribute('accept'),
'default "accept" attribute'
).to.equal('text/csv');
this.render(hbs`{{gh-file-uploader accept="application/zip"}}`);
await render(hbs`{{gh-file-uploader accept="application/zip"}}`);
expect(
this.$('input[type="file"]').attr('accept'),
find('input[type="file"]').getAttribute('accept'),
'specified "accept" attribute'
).to.equal('application/zip');
});
it('renders form with supplied label text', function () {
it('renders form with supplied label text', async function () {
this.set('labelText', 'My label');
this.render(hbs`{{gh-file-uploader labelText=labelText}}`);
await render(hbs`{{gh-file-uploader labelText=labelText}}`);
expect(this.$('label').text().trim(), 'label')
expect(find('label').textContent.trim(), 'label')
.to.equal('My label');
});
it('generates request to supplied endpoint', function (done) {
it('generates request to supplied endpoint', async function () {
stubSuccessfulUpload(server);
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(server.handledRequests.length).to.equal(1);
expect(server.handledRequests[0].url).to.equal('/ghost/api/v2/admin/uploads/');
done();
});
});
it('fires uploadSuccess action on successful upload', function (done) {
it('fires uploadSuccess action on successful upload', async function () {
let uploadSuccess = sinon.spy();
this.set('uploadSuccess', uploadSuccess);
stubSuccessfulUpload(server);
this.render(hbs`{{gh-file-uploader url=uploadUrl uploadSuccess=(action uploadSuccess)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl uploadSuccess=(action uploadSuccess)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(uploadSuccess.calledOnce).to.be.true;
expect(uploadSuccess.firstCall.args[0]).to.equal('/content/images/test.png');
done();
});
});
it('doesn\'t fire uploadSuccess action on failed upload', function (done) {
it('doesn\'t fire uploadSuccess action on failed upload', async function () {
let uploadSuccess = sinon.spy();
this.set('uploadSuccess', uploadSuccess);
stubFailedUpload(server, 500);
this.render(hbs`{{gh-file-uploader url=uploadUrl uploadSuccess=(action uploadSuccess)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl uploadSuccess=(action uploadSuccess)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
await settled();
expect(uploadSuccess.calledOnce).to.be.false;
done();
});
});
it('fires fileSelected action on file selection', function (done) {
it('fires fileSelected action on file selection', async function () {
let fileSelected = sinon.spy();
this.set('fileSelected', fileSelected);
stubSuccessfulUpload(server);
this.render(hbs`{{gh-file-uploader url=uploadUrl fileSelected=(action fileSelected)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl fileSelected=(action fileSelected)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(fileSelected.calledOnce).to.be.true;
expect(fileSelected.args[0]).to.not.be.empty;
done();
});
});
it('fires uploadStarted action on upload start', function (done) {
it('fires uploadStarted action on upload start', async function () {
let uploadStarted = sinon.spy();
this.set('uploadStarted', uploadStarted);
stubSuccessfulUpload(server);
this.render(hbs`{{gh-file-uploader url=uploadUrl uploadStarted=(action uploadStarted)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl uploadStarted=(action uploadStarted)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(uploadStarted.calledOnce).to.be.true;
done();
});
});
it('fires uploadFinished action on successful upload', function (done) {
it('fires uploadFinished action on successful upload', async function () {
let uploadFinished = sinon.spy();
this.set('uploadFinished', uploadFinished);
stubSuccessfulUpload(server);
this.render(hbs`{{gh-file-uploader url=uploadUrl uploadFinished=(action uploadFinished)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl uploadFinished=(action uploadFinished)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(uploadFinished.calledOnce).to.be.true;
done();
});
});
it('fires uploadFinished action on failed upload', function (done) {
it('fires uploadFinished action on failed upload', async function () {
let uploadFinished = sinon.spy();
this.set('uploadFinished', uploadFinished);
stubFailedUpload(server);
this.render(hbs`{{gh-file-uploader url=uploadUrl uploadFinished=(action uploadFinished)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl uploadFinished=(action uploadFinished)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(uploadFinished.calledOnce).to.be.true;
done();
});
});
it('displays invalid file type error', function (done) {
it('displays invalid file type error', async function () {
stubFailedUpload(server, 415, 'UnsupportedMediaTypeError');
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/The file type you uploaded is not supported/);
expect(this.$('.gh-btn-green').length, 'reset button is displayed').to.equal(1);
expect(this.$('.gh-btn-green').text()).to.equal('Try Again');
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/The file type you uploaded is not supported/);
expect(findAll('.gh-btn-green').length, 'reset button is displayed').to.equal(1);
expect(find('.gh-btn-green').textContent).to.equal('Try Again');
});
it('displays file too large for server error', function (done) {
it('displays file too large for server error', async function () {
stubFailedUpload(server, 413, 'RequestEntityTooLargeError');
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/The file you uploaded was larger/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/The file you uploaded was larger/);
});
it('handles file too large error directly from the web server', function (done) {
it('handles file too large error directly from the web server', async function () {
server.post('/ghost/api/v2/admin/uploads/', function () {
return [413, {}, ''];
});
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/The file you uploaded was larger/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/The file you uploaded was larger/);
});
it('displays other server-side error with message', function (done) {
it('displays other server-side error with message', async function () {
stubFailedUpload(server, 400, 'UnknownError');
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/Error: UnknownError/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/Error: UnknownError/);
});
it('handles unknown failure', function (done) {
it('handles unknown failure', async function () {
server.post('/ghost/api/v2/admin/uploads/', function () {
return [500, {'Content-Type': 'application/json'}, ''];
});
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/Something went wrong/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/Something went wrong/);
});
it('triggers notifications.showAPIError for VersionMismatchError', function (done) {
it('triggers notifications.showAPIError for VersionMismatchError', async function () {
let showAPIError = sinon.spy();
this.set('notifications.showAPIError', showAPIError);
let notifications = this.owner.lookup('service:notifications');
notifications.set('showAPIError', showAPIError);
stubFailedUpload(server, 400, 'VersionMismatchError');
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(showAPIError.calledOnce).to.be.true;
done();
});
});
it('doesn\'t trigger notifications.showAPIError for other errors', function (done) {
it('doesn\'t trigger notifications.showAPIError for other errors', async function () {
let showAPIError = sinon.spy();
this.set('notifications.showAPIError', showAPIError);
let notifications = this.owner.lookup('service:notifications');
notifications.set('showAPIError', showAPIError);
stubFailedUpload(server, 400, 'UnknownError');
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(showAPIError.called).to.be.false;
done();
});
});
it('can be reset after a failed upload', function (done) {
it('can be reset after a failed upload', async function () {
stubFailedUpload(server, 400, 'UnknownError');
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
await click('.gh-btn-green');
wait().then(() => {
run(() => {
this.$('.gh-btn-green').click();
});
expect(findAll('input[type="file"]').length).to.equal(1);
});
wait().then(() => {
expect(this.$('input[type="file"]').length).to.equal(1);
done();
});
});
it('displays upload progress', function (done) {
this.set('done', done);
it('displays upload progress', async function () {
// pretender fires a progress event every 50ms
stubSuccessfulUpload(server, 150);
this.render(hbs`{{gh-file-uploader url=uploadUrl uploadFinished=(action done)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
// TODO: replace with waitFor/waitUntil helpers
// after 75ms we should have had one progress event
run.later(this, function () {
expect(this.$('.progress .bar').length).to.equal(1);
let [, percentageWidth] = this.$('.progress .bar').attr('style').match(/width: (\d+)%?/);
expect(findAll('.progress .bar').length).to.equal(1);
let [, percentageWidth] = find('.progress .bar').getAttribute('style').match(/width: (\d+)%?/);
percentageWidth = Number.parseInt(percentageWidth);
expect(percentageWidth).to.be.above(0);
expect(percentageWidth).to.be.below(100);
}, 75);
await settled();
});
it('handles drag over/leave', function () {
this.render(hbs`{{gh-file-uploader}}`);
it('handles drag over/leave', async function () {
await render(hbs`{{gh-file-uploader}}`);
run(() => {
// eslint-disable-next-line new-cap
@ -331,16 +282,16 @@ describe('Integration: Component: gh-file-uploader', function () {
this.$('.gh-image-uploader').trigger(dragover);
});
expect(this.$('.gh-image-uploader').hasClass('-drag-over'), 'has drag-over class').to.be.true;
await settled();
run(() => {
this.$('.gh-image-uploader').trigger('dragleave');
expect(find('.gh-image-uploader').classList.contains('-drag-over'), 'has drag-over class').to.be.true;
await triggerEvent('.gh-image-uploader', 'dragleave');
expect(find('.gh-image-uploader').classList.contains('-drag-over'), 'has drag-over class').to.be.false;
});
expect(this.$('.gh-image-uploader').hasClass('-drag-over'), 'has drag-over class').to.be.false;
});
it('triggers file upload on file drop', function (done) {
it('triggers file upload on file drop', async function () {
let uploadSuccess = sinon.spy();
// eslint-disable-next-line new-cap
let drop = $.Event('drop', {
@ -352,20 +303,19 @@ describe('Integration: Component: gh-file-uploader', function () {
this.set('uploadSuccess', uploadSuccess);
stubSuccessfulUpload(server);
this.render(hbs`{{gh-file-uploader url=uploadUrl uploadSuccess=(action uploadSuccess)}}`);
await render(hbs`{{gh-file-uploader url=uploadUrl uploadSuccess=(action uploadSuccess)}}`);
run(() => {
this.$('.gh-image-uploader').trigger(drop);
});
wait().then(() => {
await settled();
expect(uploadSuccess.calledOnce).to.be.true;
expect(uploadSuccess.firstCall.args[0]).to.equal('/content/images/test.png');
done();
});
});
it('validates extension by default', function (done) {
it('validates extension by default', async function () {
let uploadSuccess = sinon.spy();
let uploadFailed = sinon.spy();
@ -374,23 +324,20 @@ describe('Integration: Component: gh-file-uploader', function () {
stubSuccessfulUpload(server);
this.render(hbs`{{gh-file-uploader
await render(hbs`{{gh-file-uploader
url=uploadUrl
uploadSuccess=(action uploadSuccess)
uploadFailed=(action uploadFailed)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.txt'});
await fileUpload('input[type="file"]', ['test'], {name: 'test.txt'});
wait().then(() => {
expect(uploadSuccess.called).to.be.false;
expect(uploadFailed.calledOnce).to.be.true;
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/The file type you uploaded is not supported/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/The file type you uploaded is not supported/);
});
it('uploads if validate action supplied and returns true', function (done) {
it('uploads if validate action supplied and returns true', async function () {
let validate = sinon.stub().returns(true);
let uploadSuccess = sinon.spy();
@ -399,21 +346,20 @@ describe('Integration: Component: gh-file-uploader', function () {
stubSuccessfulUpload(server);
this.render(hbs`{{gh-file-uploader
await render(hbs`{{gh-file-uploader
url=uploadUrl
uploadSuccess=(action uploadSuccess)
validate=(action validate)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
await settled();
wait().then(() => {
expect(validate.calledOnce).to.be.true;
expect(uploadSuccess.calledOnce).to.be.true;
done();
});
});
it('skips upload and displays error if validate action supplied and doesn\'t return true', function (done) {
it('skips upload and displays error if validate action supplied and doesn\'t return true', async function () {
let validate = sinon.stub().returns(new UnsupportedMediaTypeError());
let uploadSuccess = sinon.spy();
let uploadFailed = sinon.spy();
@ -424,21 +370,18 @@ describe('Integration: Component: gh-file-uploader', function () {
stubSuccessfulUpload(server);
this.render(hbs`{{gh-file-uploader
await render(hbs`{{gh-file-uploader
url=uploadUrl
uploadSuccess=(action uploadSuccess)
uploadFailed=(action uploadFailed)
validate=(action validate)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'});
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
wait().then(() => {
expect(validate.calledOnce).to.be.true;
expect(uploadSuccess.called).to.be.false;
expect(uploadFailed.calledOnce).to.be.true;
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/The file type you uploaded is not supported/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/The file type you uploaded is not supported/);
});
});

View file

@ -3,13 +3,13 @@ import Pretender from 'pretender';
import Service from '@ember/service';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import wait from 'ember-test-helpers/wait';
import {UnsupportedMediaTypeError} from 'ghost-admin/services/ajax';
import {click, find, findAll, render, settled, triggerEvent} from '@ember/test-helpers';
import {createFile, fileUpload} from '../../helpers/file-upload';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
const notificationsStub = Service.extend({
showAPIError(/* error, options */) {
@ -46,17 +46,13 @@ const stubFailedUpload = function (server, code, error, delay = 0) {
};
describe('Integration: Component: gh-image-uploader', function () {
setupComponentTest('gh-image-upload', {
integration: true
});
setupRenderingTest();
let server;
beforeEach(function () {
this.register('service:session', sessionStub);
this.register('service:notifications', notificationsStub);
this.inject.service('session', {as: 'sessionService'});
this.inject.service('notifications', {as: 'notifications'});
this.owner.register('service:session', sessionStub);
this.owner.register('service:notifications', notificationsStub);
this.set('update', function () {});
server = new Pretender();
});
@ -65,263 +61,215 @@ describe('Integration: Component: gh-image-uploader', function () {
server.shutdown();
});
it('renders', function () {
it('renders', async function () {
this.set('image', 'http://example.com/test.png');
this.render(hbs`{{gh-image-uploader image=image}}`);
await render(hbs`{{gh-image-uploader image=image}}`);
expect(this.$()).to.have.length(1);
});
it('renders form with supplied alt text', function () {
this.render(hbs`{{gh-image-uploader image=image altText="text test"}}`);
expect(this.$('[data-test-file-input-description]').text().trim()).to.equal('Upload image of "text test"');
it('renders form with supplied alt text', async function () {
await render(hbs`{{gh-image-uploader image=image altText="text test"}}`);
expect(find('[data-test-file-input-description]')).to.have.trimmed.text('Upload image of "text test"');
});
it('renders form with supplied text', function () {
this.render(hbs`{{gh-image-uploader image=image text="text test"}}`);
expect(this.$('[data-test-file-input-description]').text().trim()).to.equal('text test');
it('renders form with supplied text', async function () {
await render(hbs`{{gh-image-uploader image=image text="text test"}}`);
expect(find('[data-test-file-input-description]')).to.have.trimmed.text('text test');
});
it('generates request to correct endpoint', function (done) {
it('generates request to correct endpoint', async function () {
stubSuccessfulUpload(server);
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(server.handledRequests.length).to.equal(1);
expect(server.handledRequests[0].url).to.equal('/ghost/api/v2/admin/uploads/');
expect(server.handledRequests[0].requestHeaders.Authorization).to.be.undefined;
done();
});
});
it('fires update action on successful upload', function (done) {
it('fires update action on successful upload', async function () {
let update = sinon.spy();
this.set('update', update);
stubSuccessfulUpload(server);
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(update.calledOnce).to.be.true;
expect(update.firstCall.args[0]).to.equal('/content/images/test.png');
done();
});
});
it('doesn\'t fire update action on failed upload', function (done) {
it('doesn\'t fire update action on failed upload', async function () {
let update = sinon.spy();
this.set('update', update);
stubFailedUpload(server, 500);
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(update.calledOnce).to.be.false;
done();
});
});
it('fires fileSelected action on file selection', function (done) {
it('fires fileSelected action on file selection', async function () {
let fileSelected = sinon.spy();
this.set('fileSelected', fileSelected);
stubSuccessfulUpload(server);
this.render(hbs`{{gh-image-uploader image=image fileSelected=(action fileSelected) update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image fileSelected=(action fileSelected) update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(fileSelected.calledOnce).to.be.true;
expect(fileSelected.args[0]).to.not.be.empty;
done();
});
});
it('fires uploadStarted action on upload start', function (done) {
it('fires uploadStarted action on upload start', async function () {
let uploadStarted = sinon.spy();
this.set('uploadStarted', uploadStarted);
stubSuccessfulUpload(server);
this.render(hbs`{{gh-image-uploader image=image uploadStarted=(action uploadStarted) update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image uploadStarted=(action uploadStarted) update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(uploadStarted.calledOnce).to.be.true;
done();
});
});
it('fires uploadFinished action on successful upload', function (done) {
it('fires uploadFinished action on successful upload', async function () {
let uploadFinished = sinon.spy();
this.set('uploadFinished', uploadFinished);
stubSuccessfulUpload(server);
this.render(hbs`{{gh-image-uploader image=image uploadFinished=(action uploadFinished) update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image uploadFinished=(action uploadFinished) update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(uploadFinished.calledOnce).to.be.true;
done();
});
});
it('fires uploadFinished action on failed upload', function (done) {
it('fires uploadFinished action on failed upload', async function () {
let uploadFinished = sinon.spy();
this.set('uploadFinished', uploadFinished);
stubFailedUpload(server);
this.render(hbs`{{gh-image-uploader image=image uploadFinished=(action uploadFinished) update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image uploadFinished=(action uploadFinished) update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(uploadFinished.calledOnce).to.be.true;
done();
});
});
it('displays invalid file type error', function (done) {
it('displays invalid file type error', async function () {
stubFailedUpload(server, 415, 'UnsupportedMediaTypeError');
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/The image type you uploaded is not supported/);
expect(this.$('.gh-btn-green').length, 'reset button is displayed').to.equal(1);
expect(this.$('.gh-btn-green').text()).to.equal('Try Again');
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/The image type you uploaded is not supported/);
expect(findAll('.gh-btn-green').length, 'reset button is displayed').to.equal(1);
expect(find('.gh-btn-green').textContent).to.equal('Try Again');
});
it('displays file too large for server error', function (done) {
it('displays file too large for server error', async function () {
stubFailedUpload(server, 413, 'RequestEntityTooLargeError');
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/The image you uploaded was larger/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/The image you uploaded was larger/);
});
it('handles file too large error directly from the web server', function (done) {
it('handles file too large error directly from the web server', async function () {
server.post('/ghost/api/v2/admin/uploads/', function () {
return [413, {}, ''];
});
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/The image you uploaded was larger/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/The image you uploaded was larger/);
});
it('displays other server-side error with message', function (done) {
it('displays other server-side error with message', async function () {
stubFailedUpload(server, 400, 'UnknownError');
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/Error: UnknownError/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/Error: UnknownError/);
});
it('handles unknown failure', function (done) {
it('handles unknown failure', async function () {
server.post('/ghost/api/v2/admin/uploads/', function () {
return [500, {'Content-Type': 'application/json'}, ''];
});
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/Something went wrong/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/Something went wrong/);
});
it('triggers notifications.showAPIError for VersionMismatchError', function (done) {
it('triggers notifications.showAPIError for VersionMismatchError', async function () {
let showAPIError = sinon.spy();
this.set('notifications.showAPIError', showAPIError);
let notifications = this.owner.lookup('service:notifications');
notifications.set('showAPIError', showAPIError);
stubFailedUpload(server, 400, 'VersionMismatchError');
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(showAPIError.calledOnce).to.be.true;
done();
});
});
it('doesn\'t trigger notifications.showAPIError for other errors', function (done) {
it('doesn\'t trigger notifications.showAPIError for other errors', async function () {
let showAPIError = sinon.spy();
this.set('notifications.showAPIError', showAPIError);
let notifications = this.owner.lookup('service:notifications');
notifications.set('showAPIError', showAPIError);
stubFailedUpload(server, 400, 'UnknownError');
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(showAPIError.called).to.be.false;
done();
});
});
it('can be reset after a failed upload', function (done) {
it('can be reset after a failed upload', async function () {
stubFailedUpload(server, 400, 'UnknownError');
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {type: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await fileUpload('input[type="file"]', ['test'], {type: 'test.png'});
await click('.gh-btn-green');
wait().then(() => {
run(() => {
this.$('.gh-btn-green').click();
});
expect(findAll('input[type="file"]').length).to.equal(1);
});
wait().then(() => {
expect(this.$('input[type="file"]').length).to.equal(1);
done();
});
});
it('displays upload progress', function (done) {
this.set('done', done);
it('displays upload progress', async function () {
// pretender fires a progress event every 50ms
stubSuccessfulUpload(server, 150);
this.render(hbs`{{gh-image-uploader image=image uploadFinished=(action done) update=(action update)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
// after 75ms we should have had one progress event
run.later(this, function () {
expect(this.$('.progress .bar').length).to.equal(1);
let [, percentageWidth] = this.$('.progress .bar').attr('style').match(/width: (\d+)%?/);
expect(findAll('.progress .bar').length).to.equal(1);
let [, percentageWidth] = find('.progress .bar').getAttribute('style').match(/width: (\d+)%?/);
percentageWidth = Number.parseInt(percentageWidth);
expect(percentageWidth).to.be.above(0);
expect(percentageWidth).to.be.below(100);
}, 75);
await settled();
});
it('handles drag over/leave', function () {
it('handles drag over/leave', async function () {
stubSuccessfulUpload(server);
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
run(() => {
// eslint-disable-next-line new-cap
@ -332,17 +280,16 @@ describe('Integration: Component: gh-image-uploader', function () {
});
this.$('.gh-image-uploader').trigger(dragover);
});
await settled();
expect(this.$('.gh-image-uploader').hasClass('-drag-over'), 'has drag-over class').to.be.true;
expect(find('.gh-image-uploader').classList.contains('-drag-over'), 'has drag-over class').to.be.true;
run(() => {
this.$('.gh-image-uploader').trigger('dragleave');
await triggerEvent('.gh-image-uploader', 'dragleave');
expect(find('.gh-image-uploader').classList.contains('-drag-over'), 'has drag-over class').to.be.false;
});
expect(this.$('.gh-image-uploader').hasClass('-drag-over'), 'has drag-over class').to.be.false;
});
it('triggers file upload on file drop', function (done) {
it('triggers file upload on file drop', async function () {
let uploadSuccess = sinon.spy();
// eslint-disable-next-line new-cap
let drop = $.Event('drop', {
@ -354,20 +301,18 @@ describe('Integration: Component: gh-image-uploader', function () {
this.set('uploadSuccess', uploadSuccess);
stubSuccessfulUpload(server);
this.render(hbs`{{gh-image-uploader uploadSuccess=(action uploadSuccess)}}`);
await render(hbs`{{gh-image-uploader uploadSuccess=(action uploadSuccess)}}`);
run(() => {
this.$('.gh-image-uploader').trigger(drop);
});
await settled();
wait().then(() => {
expect(uploadSuccess.calledOnce).to.be.true;
expect(uploadSuccess.firstCall.args[0]).to.equal('/content/images/test.png');
done();
});
});
it('validates extension by default', function (done) {
it('validates extension by default', async function () {
let uploadSuccess = sinon.spy();
let uploadFailed = sinon.spy();
@ -376,22 +321,19 @@ describe('Integration: Component: gh-image-uploader', function () {
stubSuccessfulUpload(server);
this.render(hbs`{{gh-image-uploader
await render(hbs`{{gh-image-uploader
uploadSuccess=(action uploadSuccess)
uploadFailed=(action uploadFailed)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.json'});
await fileUpload('input[type="file"]', ['test'], {name: 'test.json'});
wait().then(() => {
expect(uploadSuccess.called).to.be.false;
expect(uploadFailed.calledOnce).to.be.true;
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/The image type you uploaded is not supported/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/The image type you uploaded is not supported/);
});
it('uploads if validate action supplied and returns true', function (done) {
it('uploads if validate action supplied and returns true', async function () {
let validate = sinon.stub().returns(true);
let uploadSuccess = sinon.spy();
@ -400,20 +342,17 @@ describe('Integration: Component: gh-image-uploader', function () {
stubSuccessfulUpload(server);
this.render(hbs`{{gh-image-uploader
await render(hbs`{{gh-image-uploader
uploadSuccess=(action uploadSuccess)
validate=(action validate)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.txt'});
await fileUpload('input[type="file"]', ['test'], {name: 'test.txt'});
wait().then(() => {
expect(validate.calledOnce).to.be.true;
expect(uploadSuccess.calledOnce).to.be.true;
done();
});
});
it('skips upload and displays error if validate action supplied and doesn\'t return true', function (done) {
it('skips upload and displays error if validate action supplied and doesn\'t return true', async function () {
let validate = sinon.stub().returns(new UnsupportedMediaTypeError());
let uploadSuccess = sinon.spy();
let uploadFailed = sinon.spy();
@ -424,21 +363,18 @@ describe('Integration: Component: gh-image-uploader', function () {
stubSuccessfulUpload(server);
this.render(hbs`{{gh-image-uploader
await render(hbs`{{gh-image-uploader
uploadSuccess=(action uploadSuccess)
uploadFailed=(action uploadFailed)
validate=(action validate)}}`);
fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'});
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
wait().then(() => {
expect(validate.calledOnce).to.be.true;
expect(uploadSuccess.called).to.be.false;
expect(uploadFailed.calledOnce).to.be.true;
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
expect(this.$('.failed').text()).to.match(/The image type you uploaded is not supported/);
done();
});
expect(findAll('.failed').length, 'error message is displayed').to.equal(1);
expect(find('.failed').textContent).to.match(/The image type you uploaded is not supported/);
});
describe('unsplash', function () {

View file

@ -1,39 +1,35 @@
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import {click, find, findAll, render} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-image-uploader-with-preview', function () {
setupComponentTest('gh-image-uploader-with-preview', {
integration: true
});
setupRenderingTest();
it('renders image if provided', function () {
it('renders image if provided', async function () {
this.set('image', 'http://example.com/test.png');
this.render(hbs`{{gh-image-uploader-with-preview image=image}}`);
await render(hbs`{{gh-image-uploader-with-preview image=image}}`);
expect(this.$('.gh-image-uploader.-with-image').length).to.equal(1);
expect(this.$('img').attr('src')).to.equal('http://example.com/test.png');
expect(findAll('.gh-image-uploader.-with-image').length).to.equal(1);
expect(find('img').getAttribute('src')).to.equal('http://example.com/test.png');
});
it('renders upload form when no image provided', function () {
this.render(hbs`{{gh-image-uploader-with-preview image=image}}`);
it('renders upload form when no image provided', async function () {
await render(hbs`{{gh-image-uploader-with-preview image=image}}`);
expect(this.$('input[type="file"]').length).to.equal(1);
expect(findAll('input[type="file"]').length).to.equal(1);
});
it('triggers remove action when delete icon is clicked', function () {
it('triggers remove action when delete icon is clicked', async function () {
let remove = sinon.spy();
this.set('remove', remove);
this.set('image', 'http://example.com/test.png');
this.render(hbs`{{gh-image-uploader-with-preview image=image remove=(action remove)}}`);
run(() => {
this.$('.image-cancel').click();
});
await render(hbs`{{gh-image-uploader-with-preview image=image remove=(action remove)}}`);
await click('.image-cancel');
expect(remove.calledOnce).to.be.true;
});

View file

@ -1,24 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-koenig-editor', function () {
setupComponentTest('gh-koenig-editor', {
integration: true
});
it('renders', function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#gh-koenig-editor}}
// template content
// {{/gh-koenig-editor}}
// `);
this.render(hbs`{{gh-koenig-editor}}`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -1,33 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-markdown-editor', function () {
setupComponentTest('gh-markdown-editor', {
integration: true
});
it('renders', function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#gh-markdown-editor}}
// template content
// {{/gh-markdown-editor}}
// `);
this.render(hbs`{{gh-markdown-editor}}`);
expect(this.$()).to.have.length(1);
});
describe('unsplash', function () {
it('has unsplash icon in toolbar if unsplash is active');
it('opens unsplash modal when clicked');
it('closes unsplash modal when close triggered');
it('inserts unsplash image & credit when selected');
it('inserts at cursor when editor has focus');
it('inserts at end when editor is blurred');
});
});

View file

@ -1,23 +1,21 @@
import NavItem from 'ghost-admin/models/navigation-item';
import hbs from 'htmlbars-inline-precompile';
import wait from 'ember-test-helpers/wait';
import {click, render, triggerEvent} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-navitem', function () {
setupComponentTest('gh-navitem', {
integration: true
});
setupRenderingTest();
beforeEach(function () {
this.set('baseUrl', 'http://localhost:2368');
});
it('renders', function () {
it('renders', async function () {
this.set('navItem', NavItem.create({label: 'Test', url: '/url'}));
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl}}`);
await render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl}}`);
let $item = this.$('.gh-blognav-item');
expect($item.find('.gh-blognav-grab').length).to.equal(1);
@ -31,90 +29,91 @@ describe('Integration: Component: gh-navitem', function () {
expect($item.find('.response:visible').length).to.equal(0);
});
it('doesn\'t show drag handle for new items', function () {
it('doesn\'t show drag handle for new items', async function () {
this.set('navItem', NavItem.create({label: 'Test', url: '/url', isNew: true}));
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl}}`);
await 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 new items', function () {
it('shows add button for new items', async function () {
this.set('navItem', NavItem.create({label: 'Test', url: '/url', isNew: true}));
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl}}`);
await 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 () {
it('triggers delete action', async function () {
this.set('navItem', NavItem.create({label: 'Test', url: '/url'}));
let deleteActionCallCount = 0;
this.on('deleteItem', (navItem) => {
this.set('deleteItem', (navItem) => {
expect(navItem).to.equal(this.get('navItem'));
deleteActionCallCount += 1;
});
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl deleteItem=(action "deleteItem")}}`);
this.$('.gh-blognav-delete').trigger('click');
await render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl deleteItem=(action deleteItem)}}`);
await click('.gh-blognav-delete');
expect(deleteActionCallCount).to.equal(1);
});
it('triggers add action', function () {
it('triggers add action', async function () {
this.set('navItem', NavItem.create({label: 'Test', url: '/url', isNew: true}));
let addActionCallCount = 0;
this.on('add', () => {
this.set('add', () => {
addActionCallCount += 1;
});
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl addItem=(action "add")}}`);
this.$('.gh-blognav-add').trigger('click');
await render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl addItem=(action add)}}`);
await click('.gh-blognav-add');
expect(addActionCallCount).to.equal(1);
});
it('triggers update url action', function () {
it('triggers update url action', async function () {
this.set('navItem', NavItem.create({label: 'Test', url: '/url'}));
let updateActionCallCount = 0;
this.on('update', () => {
this.set('update', (value) => {
updateActionCallCount += 1;
return value;
});
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl updateUrl=(action "update")}}`);
this.$('.gh-blognav-url input').trigger('blur');
await render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl updateUrl=(action update)}}`);
await triggerEvent('.gh-blognav-url input', 'blur');
expect(updateActionCallCount).to.equal(1);
});
it('triggers update label action', function () {
it('triggers update label action', async function () {
this.set('navItem', NavItem.create({label: 'Test', url: '/url'}));
let updateActionCallCount = 0;
this.on('update', () => {
this.set('update', (value) => {
updateActionCallCount += 1;
return value;
});
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl updateLabel=(action "update")}}`);
this.$('.gh-blognav-label input').trigger('blur');
await render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl updateLabel=(action update)}}`);
await triggerEvent('.gh-blognav-label input', 'blur');
expect(updateActionCallCount).to.equal(1);
});
it('displays inline errors', function () {
it('displays inline errors', async function () {
this.set('navItem', NavItem.create({label: '', url: ''}));
this.get('navItem').validate();
this.render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl}}`);
await render(hbs`{{gh-navitem navItem=navItem baseUrl=baseUrl}}`);
let $item = this.$('.gh-blognav-item');
return wait().then(() => {
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');
@ -122,4 +121,3 @@ describe('Integration: Component: gh-navitem', function () {
expect($item.find('.gh-blognav-url .response').text().trim()).to.equal('You must specify a URL or relative path');
});
});
});

View file

@ -1,423 +1,350 @@
import $ from 'jquery';
import hbs from 'htmlbars-inline-precompile';
import {blur, click, fillIn, find, findAll, render, triggerKeyEvent} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
// we want baseUrl to match the running domain so relative URLs are
// handled as expected (browser auto-sets the domain when using a.href)
let currentUrl = `${window.location.protocol}//${window.location.host}/`;
describe('Integration: Component: gh-navitem-url-input', function () {
setupComponentTest('gh-navitem-url-input', {
integration: true
});
setupRenderingTest();
beforeEach(function () {
// set defaults
this.set('baseUrl', currentUrl);
this.set('url', '');
this.set('isNew', false);
this.on('clearErrors', function () {
this.set('clearErrors', function () {
return null;
});
});
it('renders correctly with blank url', function () {
this.render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew clearErrors=(action "clearErrors")}}
it('renders correctly with blank url', async function () {
await render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
expect($input).to.have.length(1);
expect($input.hasClass('gh-input')).to.be.true;
expect($input.val()).to.equal(currentUrl);
expect(findAll('input')).to.have.length(1);
expect(find('input')).to.have.class('gh-input');
expect(find('input')).to.have.value(currentUrl);
});
it('renders correctly with relative urls', function () {
it('renders correctly with relative urls', async function () {
this.set('url', '/about');
this.render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew clearErrors=(action "clearErrors")}}
await render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
expect($input.val()).to.equal(`${currentUrl}about`);
expect(find('input')).to.have.value(`${currentUrl}about`);
this.set('url', '/about#contact');
expect($input.val()).to.equal(`${currentUrl}about#contact`);
expect(find('input')).to.have.value(`${currentUrl}about#contact`);
});
it('renders correctly with absolute urls', function () {
it('renders correctly with absolute urls', async function () {
this.set('url', 'https://example.com:2368/#test');
this.render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew clearErrors=(action "clearErrors")}}
await render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
expect($input.val()).to.equal('https://example.com:2368/#test');
expect(find('input')).to.have.value('https://example.com:2368/#test');
this.set('url', 'mailto:test@example.com');
expect($input.val()).to.equal('mailto:test@example.com');
expect(find('input')).to.have.value('mailto:test@example.com');
this.set('url', 'tel:01234-5678-90');
expect($input.val()).to.equal('tel:01234-5678-90');
expect(find('input')).to.have.value('tel:01234-5678-90');
this.set('url', '//protocol-less-url.com');
expect($input.val()).to.equal('//protocol-less-url.com');
expect(find('input')).to.have.value('//protocol-less-url.com');
this.set('url', '#anchor');
expect($input.val()).to.equal('#anchor');
expect(find('input')).to.have.value('#anchor');
});
it('deletes base URL on backspace', function () {
this.render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew clearErrors=(action "clearErrors")}}
it('deletes base URL on backspace', async function () {
await render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
expect($input.val()).to.equal(currentUrl);
run(() => {
// TODO: why is ember's keyEvent helper not available here?
// eslint-disable-next-line new-cap
let e = $.Event('keydown');
e.keyCode = 8;
$input.trigger(e);
});
expect($input.val()).to.equal('');
expect(find('input')).to.have.value(currentUrl);
await triggerKeyEvent('input', 'keydown', 8);
expect(find('input')).to.have.value('');
});
it('deletes base URL on delete', function () {
this.render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew clearErrors=(action "clearErrors")}}
it('deletes base URL on delete', async function () {
await render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
expect($input.val()).to.equal(currentUrl);
run(() => {
// TODO: why is ember's keyEvent helper not available here?
// eslint-disable-next-line new-cap
let e = $.Event('keydown');
e.keyCode = 46;
$input.trigger(e);
});
expect($input.val()).to.equal('');
expect(find('input')).to.have.value(currentUrl);
await triggerKeyEvent('input', 'keydown', 46);
expect(find('input')).to.have.value('');
});
it('adds base url to relative urls on blur', function () {
this.on('updateUrl', () => null);
this.render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
it('adds base url to relative urls on blur', async function () {
this.set('updateUrl', val => val);
await render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
run(() => {
$input.val('/about').trigger('input');
});
run(() => {
$input.trigger('blur');
await fillIn('input', '/about');
await blur('input');
expect(find('input')).to.have.value(`${currentUrl}about/`);
});
expect($input.val()).to.equal(`${currentUrl}about`);
});
it('adds "mailto:" to email addresses on blur', function () {
this.on('updateUrl', () => null);
this.render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
it('adds "mailto:" to email addresses on blur', async function () {
this.set('updateUrl', val => val);
await render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
run(() => {
$input.val('test@example.com').trigger('input');
});
run(() => {
$input.trigger('blur');
});
await fillIn('input', 'test@example.com');
await blur('input');
expect($input.val()).to.equal('mailto:test@example.com');
expect(find('input')).to.have.value('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');
await blur('input');
expect(find('input')).to.have.value('mailto:test@example.com');
});
it('doesn\'t add base url to invalid urls on blur', function () {
this.on('updateUrl', () => null);
this.render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
it('doesn\'t add base url to invalid urls on blur', async function () {
this.set('updateUrl', val => val);
await render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
let changeValue = function (value) {
run(() => {
$input.val(value).trigger('input').trigger('blur');
});
let changeValue = async (value) => {
await fillIn('input', value);
await blur('input');
};
changeValue('with spaces');
expect($input.val()).to.equal('with spaces');
await changeValue('with spaces');
expect(find('input')).to.have.value('with spaces');
changeValue('/with spaces');
expect($input.val()).to.equal('/with spaces');
await changeValue('/with spaces');
expect(find('input')).to.have.value('/with spaces');
});
it('doesn\'t mangle invalid urls on blur', function () {
this.on('updateUrl', () => null);
this.render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
it('doesn\'t mangle invalid urls on blur', async function () {
this.set('updateUrl', val => val);
await render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
run(() => {
$input.val(`${currentUrl} /test`).trigger('input').trigger('blur');
});
await fillIn('input', `${currentUrl} /test`);
await blur('input');
expect($input.val()).to.equal(`${currentUrl} /test`);
expect(find('input')).to.have.value(`${currentUrl} /test`);
});
// https://github.com/TryGhost/Ghost/issues/9373
it('doesn\'t mangle urls when baseUrl has unicode characters', function () {
this.on('updateUrl', () => null);
it('doesn\'t mangle urls when baseUrl has unicode characters', async function () {
this.set('updateUrl', val => val);
this.set('baseUrl', 'http://exämple.com');
this.render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs`
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
await fillIn('input', `${currentUrl}/test`);
await blur('input');
run(() => {
$input.val(`${currentUrl}/test`).trigger('input').trigger('blur');
expect(find('input')).to.have.value(`${currentUrl}/test`);
});
expect($input.val()).to.equal(`${currentUrl}/test`);
});
it('triggers "update" action on blur', function () {
it('triggers "update" action on blur', async function () {
let changeActionCallCount = 0;
this.on('updateUrl', () => {
this.set('updateUrl', (val) => {
changeActionCallCount += 1;
return val;
});
this.render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
$input.trigger('blur');
await click('input');
await blur('input');
expect(changeActionCallCount).to.equal(1);
});
it('triggers "update" action on enter', function () {
it('triggers "update" action on enter', async function () {
let changeActionCallCount = 0;
this.on('updateUrl', () => {
this.set('updateUrl', (val) => {
changeActionCallCount += 1;
return val;
});
this.render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
await triggerKeyEvent('input', 'keypress', 13);
run(() => {
// TODO: why is ember's keyEvent helper not available here?
// eslint-disable-next-line new-cap
let e = $.Event('keypress');
e.keyCode = 13;
$input.trigger(e);
expect(changeActionCallCount).to.equal(1);
});
it('triggers "update" action on CMD-S', async function () {
let changeActionCallCount = 0;
this.set('updateUrl', (val) => {
changeActionCallCount += 1;
return val;
});
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
await triggerKeyEvent('input', 'keydown', 83, {
metaKey: true
});
expect(changeActionCallCount).to.equal(1);
});
it('triggers "update" action on CMD-S', function () {
let changeActionCallCount = 0;
this.on('updateUrl', () => {
changeActionCallCount += 1;
it('sends absolute urls straight through to update action', async function () {
let lastSeenUrl = '';
this.set('updateUrl', (url) => {
lastSeenUrl = url;
return url;
});
this.render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
run(() => {
// TODO: why is ember's keyEvent helper not available here?
// eslint-disable-next-line new-cap
let e = $.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 isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
`);
let $input = this.$('input');
let testUrl = (url) => {
expectedUrl = url;
run(() => {
$input.val(url).trigger('input');
});
run(() => {
$input.trigger('blur');
});
let testUrl = async (url) => {
await fillIn('input', url);
await blur('input');
expect(lastSeenUrl).to.equal(url);
};
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");');
await testUrl('http://example.com');
await testUrl('http://example.com/');
await testUrl('https://example.com');
await testUrl('//example.com');
await testUrl('//localhost:1234');
await testUrl('#anchor');
await testUrl('mailto:test@example.com');
await testUrl('tel:12345-567890');
await testUrl('javascript:alert("testing");');
});
it('strips base url from relative urls before sending to change action', function () {
let expectedUrl = '';
it('strips base url from relative urls before sending to update action', async function () {
let lastSeenUrl = '';
this.on('updateUrl', (url) => {
expect(url).to.equal(expectedUrl);
this.set('updateUrl', (url) => {
lastSeenUrl = url;
return url;
});
this.render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
let testUrl = (url) => {
expectedUrl = `/${url}`;
run(() => {
$input.val(`${currentUrl}${url}`).trigger('input');
});
run(() => {
$input.trigger('blur');
});
let testUrl = async (url) => {
await fillIn('input', `${currentUrl}${url}`);
await blur('input');
expect(lastSeenUrl).to.equal(`/${url}`);
};
testUrl('about/');
testUrl('about#contact');
testUrl('test/nested/');
await testUrl('about/');
await testUrl('about#contact');
await testUrl('test/nested/');
});
it('handles links to subdomains of blog domain', function () {
it('handles links to subdomains of blog domain', async function () {
let expectedUrl = '';
this.set('baseUrl', 'http://example.com/');
this.on('updateUrl', (url) => {
this.set('updateUrl', (url) => {
expect(url).to.equal(expectedUrl);
return url;
});
this.render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
expectedUrl = 'http://test.example.com/';
run(() => {
$input.val(expectedUrl).trigger('input').trigger('blur');
});
expect($input.val()).to.equal(expectedUrl);
await fillIn('input', expectedUrl);
await blur('input');
expect(find('input')).to.have.value(expectedUrl);
});
it('adds trailing slash to relative URL', function () {
let expectedUrl = '';
it('adds trailing slash to relative URL', async function () {
let lastSeenUrl = '';
this.on('updateUrl', (url) => {
expect(url).to.equal(expectedUrl);
this.set('updateUrl', (url) => {
lastSeenUrl = url;
return url;
});
this.render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
let testUrl = (url) => {
expectedUrl = `/${url}/`;
run(() => {
$input.val(`${currentUrl}${url}`).trigger('input');
});
run(() => {
$input.trigger('blur');
});
let testUrl = async (url) => {
await fillIn('input', `${currentUrl}${url}`);
await blur('input');
expect(lastSeenUrl).to.equal(`/${url}/`);
};
testUrl('about');
testUrl('test/nested');
await testUrl('about');
await testUrl('test/nested');
});
it('does not add trailing slash on relative URL with [.?#]', function () {
let expectedUrl = '';
it('does not add trailing slash on relative URL with [.?#]', async function () {
let lastSeenUrl = '';
this.on('updateUrl', (url) => {
expect(url).to.equal(expectedUrl);
this.set('updateUrl', (url) => {
lastSeenUrl = url;
return url;
});
this.render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
let testUrl = (url) => {
expectedUrl = `/${url}`;
run(() => {
$input.val(`${currentUrl}${url}`).trigger('input');
});
run(() => {
$input.trigger('blur');
});
let testUrl = async (url) => {
await fillIn('input', `${currentUrl}${url}`);
await blur('input');
expect(lastSeenUrl).to.equal(`/${url}`);
};
testUrl('about#contact');
testUrl('test/nested.svg');
testUrl('test?gho=sties');
testUrl('test/nested?sli=mer');
await testUrl('about#contact');
await testUrl('test/nested.svg');
await testUrl('test?gho=sties');
await testUrl('test/nested?sli=mer');
});
it('does not add trailing slash on non-relative URLs', function () {
let expectedUrl = '';
it('does not add trailing slash on non-relative URLs', async function () {
let lastSeenUrl = '';
this.on('updateUrl', (url) => {
expect(url).to.equal(expectedUrl);
this.set('updateUrl', (url) => {
lastSeenUrl = url;
return url;
});
this.render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
let testUrl = (url) => {
expectedUrl = `/${url}`;
run(() => {
$input.val(`${currentUrl}${url}`).trigger('input');
});
run(() => {
$input.trigger('blur');
});
let testUrl = async (url) => {
await fillIn('input', url);
await blur('input');
expect(lastSeenUrl).to.equal(url);
};
testUrl('http://woo.ff/test');
testUrl('http://me.ow:2342/nested/test');
testUrl('https://wro.om/car#race');
testUrl('https://kabo.om/explosion?really=now');
await testUrl('http://woo.ff/test');
await testUrl('http://me.ow:2342/nested/test');
await testUrl('https://wro.om/car#race');
await testUrl('https://kabo.om/explosion?really=now');
});
describe('with sub-folder baseUrl', function () {
@ -425,65 +352,57 @@ describe('Integration: Component: gh-navitem-url-input', function () {
this.set('baseUrl', `${currentUrl}blog/`);
});
it('handles URLs relative to base url', function () {
let expectedUrl = '';
it('handles URLs relative to base url', async function () {
let lastSeenUrl = '';
this.on('updateUrl', (url) => {
expect(url).to.equal(expectedUrl);
this.set('updateUrl', (url) => {
lastSeenUrl = url;
return url;
});
this.render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
let testUrl = (url) => {
expectedUrl = url;
run(() => {
$input.val(`${currentUrl}blog${url}`).trigger('input');
});
run(() => {
$input.trigger('blur');
});
let testUrl = async (url) => {
await fillIn('input', `${currentUrl}blog${url}`);
await blur('input');
expect(lastSeenUrl).to.equal(url);
};
testUrl('/about/');
testUrl('/about#contact');
testUrl('/test/nested/');
await testUrl('/about/');
await testUrl('/about#contact');
await testUrl('/test/nested/');
});
it('handles URLs relative to base host', function () {
let expectedUrl = '';
it('handles URLs relative to base host', async function () {
let lastSeenUrl = '';
this.on('updateUrl', (url) => {
expect(url).to.equal(expectedUrl);
this.set('updateUrl', (url) => {
lastSeenUrl = url;
return url;
});
this.render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action "updateUrl") clearErrors=(action "clearErrors")}}
await render(hbs `
{{gh-navitem-url-input baseUrl=baseUrl url=url isNew=isNew update=(action updateUrl) clearErrors=(action clearErrors)}}
`);
let $input = this.$('input');
let testUrl = (url) => {
expectedUrl = url;
run(() => {
$input.val(url).trigger('input');
});
run(() => {
$input.trigger('blur');
});
let testUrl = async (url) => {
await fillIn('input', url);
await blur('input');
expect(lastSeenUrl).to.equal(url);
};
testUrl(`http://${window.location.host}`);
testUrl(`https://${window.location.host}`);
testUrl(`http://${window.location.host}/`);
testUrl(`https://${window.location.host}/`);
testUrl(`http://${window.location.host}/test`);
testUrl(`https://${window.location.host}/test`);
testUrl(`http://${window.location.host}/#test`);
testUrl(`https://${window.location.host}/#test`);
testUrl(`http://${window.location.host}/another/folder`);
testUrl(`https://${window.location.host}/another/folder`);
await testUrl(`http://${window.location.host}`);
await testUrl(`https://${window.location.host}`);
await testUrl(`http://${window.location.host}/`);
await testUrl(`https://${window.location.host}/`);
await testUrl(`http://${window.location.host}/test`);
await testUrl(`https://${window.location.host}/test`);
await testUrl(`http://${window.location.host}/#test`);
await testUrl(`https://${window.location.host}/#test`);
await testUrl(`http://${window.location.host}/another/folder`);
await testUrl(`https://${window.location.host}/another/folder`);
});
});
});

View file

@ -1,38 +1,40 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {find, render} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-notification', function () {
setupComponentTest('gh-notification', {
integration: true
});
setupRenderingTest();
it('renders', function () {
it('renders', async function () {
this.set('message', {message: 'Test message', type: 'success'});
this.render(hbs`{{gh-notification message=message}}`);
await render(hbs`{{gh-notification message=message}}`);
expect(this.$('article.gh-notification')).to.have.length(1);
let $notification = this.$('.gh-notification');
expect(find('article.gh-notification')).to.exist;
expect($notification.hasClass('gh-notification-passive')).to.be.true;
expect($notification.text()).to.match(/Test message/);
let notification = find('.gh-notification');
expect(notification).to.have.class('gh-notification-passive');
expect(notification).to.contain.text('Test message');
});
it('maps message types to CSS classes', function () {
it('maps message types to CSS classes', async function () {
this.set('message', {message: 'Test message', type: 'success'});
this.render(hbs`{{gh-notification message=message}}`);
let $notification = this.$('.gh-notification');
await render(hbs`{{gh-notification message=message}}`);
let notification = find('.gh-notification');
this.set('message.type', 'success');
expect($notification.hasClass('gh-notification-green'), 'success class isn\'t green').to.be.true;
expect(notification, 'success class is green')
.to.have.class('gh-notification-green');
this.set('message.type', 'error');
expect($notification.hasClass('gh-notification-red'), 'success class isn\'t red').to.be.true;
expect(notification, 'success class is red')
.to.have.class('gh-notification-red');
this.set('message.type', 'warn');
expect($notification.hasClass('gh-notification-yellow'), 'success class isn\'t yellow').to.be.true;
expect(notification, 'success class is yellow')
.to.have.class('gh-notification-yellow');
});
});

View file

@ -3,34 +3,35 @@ import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {A as emberA} from '@ember/array';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {find, render, settled} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
let notificationsStub = Service.extend({
notifications: emberA()
});
describe('Integration: Component: gh-notifications', function () {
setupComponentTest('gh-notifications', {
integration: true
});
setupRenderingTest();
beforeEach(function () {
this.register('service:notifications', notificationsStub);
this.inject.service('notifications', {as: 'notifications'});
this.owner.register('service:notifications', notificationsStub);
let notifications = this.owner.lookup('service:notifications');
this.set('notifications.notifications', [
notifications.set('notifications', [
{message: 'First', type: 'error'},
{message: 'Second', type: 'warn'}
]);
});
it('renders', function () {
this.render(hbs`{{gh-notifications}}`);
expect(this.$('.gh-notifications').length).to.equal(1);
it('renders', async function () {
await render(hbs`{{gh-notifications}}`);
expect(find('.gh-notifications')).to.exist;
expect(this.$('.gh-notifications').children().length).to.equal(2);
expect(find('.gh-notifications').children.length).to.equal(2);
this.set('notifications.notifications', emberA());
expect(this.$('.gh-notifications').children().length).to.equal(0);
let notifications = this.owner.lookup('service:notifications');
notifications.set('notifications', emberA());
await settled();
expect(find('.gh-notifications').children.length).to.equal(0);
});
});

View file

@ -2,11 +2,10 @@ import Pretender from 'pretender';
import Service from '@ember/service';
import hbs from 'htmlbars-inline-precompile';
import md5 from 'npm:blueimp-md5';
import wait from 'ember-test-helpers/wait';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {find, render} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
import {timeout} from 'ember-concurrency';
let pathsStub = Service.extend({
@ -51,17 +50,13 @@ let configStubuseGravatar = Service.extend({
});
describe('Integration: Component: gh-profile-image', function () {
setupComponentTest('gh-profile-image', {
integration: true
});
setupRenderingTest();
let server;
beforeEach(function () {
this.register('service:ghost-paths', pathsStub);
this.inject.service('ghost-paths', {as: 'ghost-paths'});
this.register('service:config', configStubuseGravatar);
this.inject.service('config', {as: 'config'});
this.owner.register('service:ghost-paths', pathsStub);
this.owner.register('service:config', configStubuseGravatar);
server = new Pretender();
stubKnownGravatar(server);
@ -71,25 +66,29 @@ describe('Integration: Component: gh-profile-image', function () {
server.shutdown();
});
it('renders', function () {
it('renders', async function () {
this.set('email', '');
this.render(hbs`
await render(hbs`
{{gh-profile-image email=email}}
`);
expect(this.$()).to.have.length(1);
expect(find('.account-image')).to.exist;
expect(find('.placeholder-img')).to.exist;
expect(find('input[type="file"]')).to.exist;
});
it('renders default image if no email supplied', function () {
it('renders default image if no email supplied', async function () {
this.set('email', null);
this.render(hbs`
await render(hbs`
{{gh-profile-image email=email size=100 debounce=50}}
`);
expect(this.$('.gravatar-img').attr('style'), 'gravatar image style')
.to.equal('display: none');
expect(
find('.gravatar-img'),
'gravatar image style'
).to.have.attribute('style', 'display: none');
});
it('renders the gravatar if valid email supplied and privacy.useGravatar allows it', async function () {
@ -98,44 +97,44 @@ describe('Integration: Component: gh-profile-image', function () {
this.set('email', email);
this.render(hbs`
await render(hbs`
{{gh-profile-image email=email size=100 debounce=50}}
`);
// wait for the ajax request to complete
await wait();
expect(this.$('.gravatar-img').attr('style'), 'gravatar image style')
.to.equal(`background-image: url(${expectedUrl}); display: block`);
expect(
find('.gravatar-img'),
'gravatar image style'
).to.have.attribute('style', `background-image: url(${expectedUrl}); display: block`);
});
it('doesn\'t render the gravatar if valid email supplied but privacy.useGravatar forbids it', async function () {
let config = this.owner.lookup('service:config');
let email = 'test@example.com';
this.set('email', email);
this.set('config.useGravatar', false);
config.set('useGravatar', false);
this.render(hbs`
await render(hbs`
{{gh-profile-image email=email size=100 debounce=50}}
`);
await wait();
expect(this.$('.gravatar-img').attr('style'), 'gravatar image style')
.to.equal('display: none');
expect(
find('.gravatar-img'),
'gravatar image style'
).to.have.attribute('style', 'display: none');
});
it('doesn\'t add background url if gravatar image doesn\'t exist', async function () {
stubUnknownGravatar(server);
this.render(hbs`
await render(hbs`
{{gh-profile-image email="test@example.com" size=100 debounce=50}}
`);
await wait();
expect(this.$('.gravatar-img').attr('style'), 'gravatar image style')
.to.equal('background-image: url(); display: none');
expect(
find('.gravatar-img'),
'gravatar image style'
).to.have.attribute('style', 'background-image: url(); display: none');
});
it('throttles gravatar loading as email is changed', async function () {
@ -144,25 +143,31 @@ describe('Integration: Component: gh-profile-image', function () {
this.set('email', 'test');
this.render(hbs`
await render(hbs`
{{gh-profile-image email=email size=100 debounce=300}}
`);
run(() => {
this.set('email', email);
});
expect(this.$('.gravatar-img').attr('style'), '.gravatar-img background not immediately changed on email change')
.to.equal('display: none');
await timeout(50);
expect(
find('.gravatar-img'),
'.gravatar-img background not immediately changed on email change'
).to.have.attribute('style', 'display: none');
await timeout(250);
expect(this.$('.gravatar-img').attr('style'), '.gravatar-img background still not changed before debounce timeout')
.to.equal('display: none');
expect(
find('.gravatar-img'),
'.gravatar-img background still not changed before debounce timeout'
).to.have.attribute('style', 'display: none');
await timeout(100);
expect(this.$('.gravatar-img').attr('style'), '.gravatar-img background changed after debounce timeout')
.to.equal(`background-image: url(${expectedUrl}); display: block`);
expect(
find('.gravatar-img'),
'.gravatar-img background changed after debounce timeout'
).to.have.attribute('style', `background-image: url(${expectedUrl}); display: block`);
});
});

View file

@ -1,24 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-progress-bar', function () {
setupComponentTest('gh-progress-bar', {
integration: true
});
it('renders', function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#gh-progress-bar}}
// template content
// {{/gh-progress-bar}}
// `);
this.render(hbs`{{gh-progress-bar}}`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -1,37 +1,31 @@
import hbs from 'htmlbars-inline-precompile';
import mockPosts from '../../../mirage/config/posts';
import mockTags from '../../../mirage/config/themes';
import wait from 'ember-test-helpers/wait';
import {click, findAll} from 'ember-native-dom-helpers';
import {click, findAll, render, settled} from '@ember/test-helpers';
import {clickTrigger, selectChoose, typeInSearch} from 'ember-power-select/test-support/helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
import {startMirage} from 'ghost-admin/initializers/ember-cli-mirage';
import {timeout} from 'ember-concurrency';
// NOTE: although Mirage has posts<->tags relationship and can respond
// to :post-id/?include=tags all ordering information is lost so we
// need to build the tags array manually
const assignPostWithTags = function postWithTags(context, ...slugs) {
context.get('store').findRecord('post', 1).then((post) => {
context.get('store').findAll('tag').then((tags) => {
const assignPostWithTags = async function postWithTags(context, ...slugs) {
let post = await context.store.findRecord('post', 1);
let tags = await context.store.findAll('tag');
slugs.forEach((slug) => {
post.get('tags').pushObject(tags.findBy('slug', slug));
});
context.set('post', post);
});
});
await settled();
};
// TODO: Unskip and fix
// skipped because it was failing most of the time on Travis
// see https://github.com/TryGhost/Ghost/issues/8805
describe.skip('Integration: Component: gh-psm-tags-input', function () {
setupComponentTest('gh-psm-tags-input', {
integration: true
});
describe('Integration: Component: gh-psm-tags-input', function () {
setupRenderingTest();
let server;
@ -42,13 +36,13 @@ describe.skip('Integration: Component: gh-psm-tags-input', function () {
mockPosts(server);
mockTags(server);
server.create('post', {author});
server.create('tag', {name: 'Tag One', slug: 'one'});
server.create('tag', {name: 'Tag Two', slug: 'two'});
server.create('tag', {name: 'Tag Three', slug: 'three'});
server.create('tag', {name: '#Internal Tag', visibility: 'internal', slug: 'internal'});
server.create('post', {authors: [author]});
server.create('tag', {name: 'Tag 1', slug: 'one'});
server.create('tag', {name: '#Tag 2', visibility: 'internal', slug: 'two'});
server.create('tag', {name: 'Tag 3', slug: 'three'});
server.create('tag', {name: 'Tag 4', slug: 'four'});
this.inject.service('store');
this.set('store', this.owner.lookup('service:store'));
});
afterEach(function () {
@ -56,153 +50,123 @@ describe.skip('Integration: Component: gh-psm-tags-input', function () {
});
it('shows selected tags on render', async function () {
run(() => {
assignPostWithTags(this, 'one', 'three');
});
await wait();
await this.render(hbs`{{gh-psm-tags-input post=post}}`);
await assignPostWithTags(this, 'one', 'three');
await render(hbs`{{gh-psm-tags-input post=post}}`);
let selected = findAll('.tag-token');
expect(selected.length).to.equal(2);
expect(selected[0].textContent).to.have.string('Tag One');
expect(selected[1].textContent).to.have.string('Tag Three');
expect(selected[0]).to.contain.text('Tag 1');
expect(selected[1]).to.contain.text('Tag 3');
});
it('exposes all tags as options sorted alphabetically', async function () {
run(() => {
this.set('post', this.get('store').findRecord('post', 1));
});
await wait();
this.set('post', this.store.findRecord('post', 1));
await settled();
await this.render(hbs`{{gh-psm-tags-input post=post}}`);
await render(hbs`{{gh-psm-tags-input post=post}}`);
await clickTrigger();
await settled();
// unsure why settled() is sometimes not catching the update
await timeout(100);
let options = findAll('.ember-power-select-option');
expect(options.length).to.equal(4);
expect(options[0].textContent).to.have.string('Tag One');
expect(options[1].textContent).to.have.string('Tag Three');
expect(options[2].textContent).to.have.string('Tag Two');
expect(options[3].textContent).to.have.string('#Internal Tag');
expect(options[0]).to.contain.text('Tag 1');
expect(options[1]).to.contain.text('#Tag 2');
expect(options[2]).to.contain.text('Tag 3');
expect(options[3]).to.contain.text('Tag 4');
});
it('matches options on lowercase tag names', async function () {
run(() => {
this.set('post', this.get('store').findRecord('post', 1));
});
await wait();
this.set('post', this.store.findRecord('post', 1));
await settled();
await this.render(hbs`{{gh-psm-tags-input post=post}}`);
await render(hbs`{{gh-psm-tags-input post=post}}`);
await clickTrigger();
await typeInSearch('two');
await typeInSearch('2');
await settled();
// unsure why settled() is sometimes not catching the update
await timeout(100);
let options = findAll('.ember-power-select-option');
expect(options.length).to.equal(2);
expect(options[0].textContent).to.have.string('Add "two"...');
expect(options[1].textContent).to.have.string('Tag Two');
expect(options[0]).to.contain.text('Add "2"...');
expect(options[1]).to.contain.text('Tag 2');
});
it('hides create option on exact matches', async function () {
run(() => {
this.set('post', this.get('store').findRecord('post', 1));
});
await wait();
this.set('post', this.store.findRecord('post', 1));
await settled();
await this.render(hbs`{{gh-psm-tags-input post=post}}`);
await render(hbs`{{gh-psm-tags-input post=post}}`);
await clickTrigger();
await typeInSearch('Tag Two');
await typeInSearch('#Tag 2');
await settled();
// unsure why settled() is sometimes not catching the update
await timeout(100);
let options = findAll('.ember-power-select-option');
expect(options.length).to.equal(1);
expect(options[0].textContent).to.have.string('Tag Two');
expect(options[0]).to.contain.text('#Tag 2');
});
describe('primary tags', function () {
it('adds primary tag class to first tag', async function () {
run(() => {
assignPostWithTags(this, 'one', 'three');
});
await wait();
await this.render(hbs`{{gh-psm-tags-input post=post}}`);
it('highlights internal tags', async function () {
await assignPostWithTags(this, 'two', 'three');
await render(hbs`{{gh-psm-tags-input post=post}}`);
let selected = findAll('.tag-token');
expect(selected.length).to.equal(2);
expect(selected[0].classList.contains('tag-token--primary')).to.be.true;
expect(selected[1].classList.contains('tag-token--primary')).to.be.false;
});
it('doesn\'t add primary tag class if first tag is internal', async function () {
run(() => {
assignPostWithTags(this, 'internal', 'two');
});
await wait();
await this.render(hbs`{{gh-psm-tags-input post=post}}`);
let selected = findAll('.tag-token');
expect(selected.length).to.equal(2);
expect(selected[0].classList.contains('tag-token--primary')).to.be.false;
expect(selected[1].classList.contains('tag-token--primary')).to.be.false;
});
expect(selected[0]).to.have.class('tag-token--internal');
expect(selected[1]).to.not.have.class('tag-token--internal');
});
describe('updateTags', function () {
it('modifies post.tags', async function () {
run(() => {
assignPostWithTags(this, 'internal', 'two');
});
await wait();
await this.render(hbs`{{gh-psm-tags-input post=post}}`);
await selectChoose('.ember-power-select-trigger', 'Tag One');
await assignPostWithTags(this, 'two', 'three');
await render(hbs`{{gh-psm-tags-input post=post}}`);
await selectChoose('.ember-power-select-trigger', 'Tag 1');
expect(
this.get('post.tags').mapBy('name').join(',')
).to.equal('#Internal Tag,Tag Two,Tag One');
this.post.tags.mapBy('name').join(',')
).to.equal('#Tag 2,Tag 3,Tag 1');
});
it('destroys new tag records when not selected', async function () {
run(() => {
assignPostWithTags(this, 'internal', 'two');
});
await wait();
await this.render(hbs`{{gh-psm-tags-input post=post}}`);
await assignPostWithTags(this, 'two', 'three');
await render(hbs`{{gh-psm-tags-input post=post}}`);
await clickTrigger();
await typeInSearch('New');
await settled();
await selectChoose('.ember-power-select-trigger', 'Add "New"...');
let tags = await this.get('store').peekAll('tag');
expect(tags.get('length')).to.equal(5);
let tags = await this.store.peekAll('tag');
expect(tags.length).to.equal(5);
let removeBtns = findAll('.ember-power-select-multiple-remove-btn');
await click(removeBtns[removeBtns.length - 1]);
tags = await this.get('store').peekAll('tag');
expect(tags.get('length')).to.equal(4);
tags = await this.store.peekAll('tag');
expect(tags.length).to.equal(4);
});
});
describe('createTag', function () {
it('creates new records', async function () {
run(() => {
assignPostWithTags(this, 'internal', 'two');
});
await wait();
await this.render(hbs`{{gh-psm-tags-input post=post}}`);
await assignPostWithTags(this, 'two', 'three');
await render(hbs`{{gh-psm-tags-input post=post}}`);
await clickTrigger();
await typeInSearch('New One');
await selectChoose('.ember-power-select-trigger', 'Add "New One"...');
await settled();
await selectChoose('.ember-power-select-trigger', '.ember-power-select-option', 0);
await typeInSearch('New Two');
await selectChoose('.ember-power-select-trigger', 'Add "New Two"...');
await settled();
await selectChoose('.ember-power-select-trigger', '.ember-power-select-option', 0);
let tags = await this.get('store').peekAll('tag');
expect(tags.get('length')).to.equal(6);
let tags = await this.store.peekAll('tag');
expect(tags.length).to.equal(6);
expect(tags.findBy('name', 'New One').get('isNew')).to.be.true;
expect(tags.findBy('name', 'New Two').get('isNew')).to.be.true;
expect(tags.findBy('name', 'New One').isNew).to.be.true;
expect(tags.findBy('name', 'New Two').isNew).to.be.true;
});
});
});

View file

@ -3,14 +3,12 @@ import mockThemes from '../../../mirage/config/themes';
import wait from 'ember-test-helpers/wait';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {find} from 'ember-native-dom-helpers';
import {setupComponentTest} from 'ember-mocha';
import {find, render} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
import {startMirage} from 'ghost-admin/initializers/ember-cli-mirage';
describe('Integration: Component: gh-psm-template-select', function () {
setupComponentTest('gh-psm-template-select', {
integration: true
});
setupRenderingTest();
let server;
@ -65,11 +63,11 @@ describe('Integration: Component: gh-psm-template-select', function () {
page: false
});
this.render(hbs`{{gh-psm-template-select post=post}}`);
await render(hbs`{{gh-psm-template-select post=post}}`);
await wait();
expect(find('select').disabled, 'select is disabled').to.be.true;
expect(find('p').textContent).to.have.string('post-one.hbs');
expect(find('p')).to.contain.text('post-one.hbs');
});
it('disables template selector if slug matches page template', async function () {
@ -78,10 +76,10 @@ describe('Integration: Component: gh-psm-template-select', function () {
page: true
});
this.render(hbs`{{gh-psm-template-select post=post}}`);
await render(hbs`{{gh-psm-template-select post=post}}`);
await wait();
expect(find('select').disabled, 'select is disabled').to.be.true;
expect(find('p').textContent).to.have.string('page-about.hbs');
expect(find('p')).to.contain.text('page-about.hbs');
});
});

View file

@ -1,24 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-publishmenu-draft', function () {
setupComponentTest('gh-publishmenu-draft', {
integration: true
});
it.skip('renders', function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#gh-publishmenu-draft}}
// template content
// {{/gh-publishmenu-draft}}
// `);
this.render(hbs`{{gh-publishmenu-draft}}`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -1,24 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-publishmenu-published', function () {
setupComponentTest('gh-publishmenu-published', {
integration: true
});
it.skip('renders', function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#gh-publishmenu-published}}
// template content
// {{/gh-publishmenu-published}}
// `);
this.render(hbs`{{gh-publishmenu-published}}`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -1,24 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-publishmenu-scheduled', function () {
setupComponentTest('gh-publishmenu-scheduled', {
integration: true
});
it.skip('renders', function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#gh-publishmenu-scheduled}}
// template content
// {{/gh-publishmenu-scheduled}}
// `);
this.render(hbs`{{gh-publishmenu-scheduled}}`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -1,30 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {startMirage} from 'ghost-admin/initializers/ember-cli-mirage';
describe('Integration: Component: gh-publishmenu', function () {
setupComponentTest('gh-publishmenu', {
integration: true
});
let server;
beforeEach(function () {
server = startMirage();
server.loadFixtures();
server.create('user');
});
afterEach(function () {
server.shutdown();
});
it('renders', function () {
this.post = server.create('post');
this.render(hbs`{{gh-publishmenu post=post}}`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -1,15 +1,12 @@
import Pretender from 'pretender';
import hbs from 'htmlbars-inline-precompile';
import wait from 'ember-test-helpers/wait';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {fillIn, findAll, render} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-search-input', function () {
setupComponentTest('gh-search-input', {
integration: true
});
setupRenderingTest();
let server;
@ -21,24 +18,17 @@ describe('Integration: Component: gh-search-input', function () {
server.shutdown();
});
it('renders', function () {
it('renders', async function () {
// renders the component on the page
this.render(hbs`{{gh-search-input}}`);
await render(hbs`{{gh-search-input}}`);
expect(this.$('.ember-power-select-search input')).to.have.length(1);
});
it('opens the dropdown on text entry', function (done) {
this.render(hbs`{{gh-search-input}}`);
it('opens the dropdown on text entry', async function () {
await render(hbs`{{gh-search-input}}`);
await fillIn('input[type="search"]', 'test');
// enter text to trigger search
run(() => {
this.$('input[type="search"]').val('test').trigger('input');
});
wait().then(() => {
expect(this.$('.ember-basic-dropdown-content').length).to.equal(1);
done();
});
expect(findAll('.ember-basic-dropdown-content').length).to.equal(1);
});
});

View file

@ -1,24 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-simplemde', function () {
setupComponentTest('gh-simplemde', {
integration: true
});
it('renders', function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#gh-simplemde}}
// template content
// {{/gh-simplemde}}
// `);
this.render(hbs`{{gh-simplemde}}`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -1,20 +0,0 @@
import Table from 'ember-light-table';
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-subscribers-table', function () {
setupComponentTest('gh-subscribers-table', {
integration: true
});
it('renders', function () {
this.set('table', new Table([], []));
this.set('sortByColumn', function () {});
this.set('delete', function () {});
this.render(hbs`{{gh-subscribers-table table=table sortByColumn=(action sortByColumn) delete=(action delete)}}`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -2,11 +2,10 @@ import DS from 'ember-data';
import EmberObject from '@ember/object';
import Service from '@ember/service';
import hbs from 'htmlbars-inline-precompile';
import wait from 'ember-test-helpers/wait';
import {blur, click, fillIn, find, findAll, render} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
const {Errors} = DS;
@ -19,9 +18,7 @@ let mediaQueriesStub = Service.extend({
});
describe('Integration: Component: gh-tag-settings-form', function () {
setupComponentTest('gh-tag-settings-form', {
integration: true
});
setupRenderingTest();
beforeEach(function () {
/* eslint-disable camelcase */
@ -38,88 +35,79 @@ describe('Integration: Component: gh-tag-settings-form', function () {
/* eslint-enable camelcase */
this.set('tag', tag);
this.set('actions.setProperty', function (property, value) {
this.set('setProperty', function (property, value) {
// this should be overridden if a call is expected
// eslint-disable-next-line no-console
console.error(`setProperty called '${property}: ${value}'`);
});
this.register('service:config', configStub);
this.inject.service('config', {as: 'config'});
this.register('service:media-queries', mediaQueriesStub);
this.inject.service('media-queries', {as: 'mediaQueries'});
this.owner.register('service:config', configStub);
this.owner.register('service:media-queries', mediaQueriesStub);
});
it('renders', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
it('renders', async function () {
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
expect(this.$()).to.have.length(1);
});
it('has the correct title', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
it('has the correct title', async function () {
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
expect(this.$('.tag-settings-pane h4').text(), 'existing tag title').to.equal('Tag Settings');
expect(find('.tag-settings-pane h4').textContent, 'existing tag title').to.equal('Tag Settings');
this.set('tag.isNew', true);
expect(this.$('.tag-settings-pane h4').text(), 'new tag title').to.equal('New Tag');
expect(find('.tag-settings-pane h4').textContent, 'new tag title').to.equal('New Tag');
});
it('renders main settings', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
it('renders main settings', async function () {
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
expect(this.$('.gh-image-uploader').length, 'displays image uploader').to.equal(1);
expect(this.$('input[name="name"]').val(), 'name field value').to.equal('Test');
expect(this.$('input[name="slug"]').val(), 'slug field value').to.equal('test');
expect(this.$('textarea[name="description"]').val(), 'description field value').to.equal('Description.');
expect(this.$('input[name="metaTitle"]').val(), 'metaTitle field value').to.equal('Meta Title');
expect(this.$('textarea[name="metaDescription"]').val(), 'metaDescription field value').to.equal('Meta description');
expect(findAll('.gh-image-uploader').length, 'displays image uploader').to.equal(1);
expect(find('input[name="name"]').value, 'name field value').to.equal('Test');
expect(find('input[name="slug"]').value, 'slug field value').to.equal('test');
expect(find('textarea[name="description"]').value, 'description field value').to.equal('Description.');
expect(find('input[name="metaTitle"]').value, 'metaTitle field value').to.equal('Meta Title');
expect(find('textarea[name="metaDescription"]').value, 'metaDescription field value').to.equal('Meta description');
});
it('can switch between main/meta settings', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
it('can switch between main/meta settings', async function () {
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
expect(this.$('.tag-settings-pane').hasClass('settings-menu-pane-in'), 'main settings are displayed by default').to.be.true;
expect(this.$('.tag-meta-settings-pane').hasClass('settings-menu-pane-out-right'), 'meta settings are hidden by default').to.be.true;
expect(find('.tag-settings-pane').classList.contains('settings-menu-pane-in'), 'main settings are displayed by default').to.be.true;
expect(find('.tag-meta-settings-pane').classList.contains('settings-menu-pane-out-right'), 'meta settings are hidden by default').to.be.true;
run(() => {
this.$('.meta-data-button').click();
await click('.meta-data-button');
expect(find('.tag-settings-pane').classList.contains('settings-menu-pane-out-left'), 'main settings are hidden after clicking Meta Data button').to.be.true;
expect(find('.tag-meta-settings-pane').classList.contains('settings-menu-pane-in'), 'meta settings are displayed after clicking Meta Data button').to.be.true;
await click('.back');
expect(find('.tag-settings-pane').classList.contains('settings-menu-pane-in'), 'main settings are displayed after clicking "back"').to.be.true;
expect(find('.tag-meta-settings-pane').classList.contains('settings-menu-pane-out-right'), 'meta settings are hidden after clicking "back"').to.be.true;
});
expect(this.$('.tag-settings-pane').hasClass('settings-menu-pane-out-left'), 'main settings are hidden after clicking Meta Data button').to.be.true;
expect(this.$('.tag-meta-settings-pane').hasClass('settings-menu-pane-in'), 'meta settings are displayed after clicking Meta Data button').to.be.true;
run(() => {
this.$('.back').click();
});
expect(this.$('.tag-settings-pane').hasClass('settings-menu-pane-in'), 'main settings are displayed after clicking "back"').to.be.true;
expect(this.$('.tag-meta-settings-pane').hasClass('settings-menu-pane-out-right'), 'meta settings are hidden after clicking "back"').to.be.true;
});
it('has one-way binding for properties', function () {
this.set('actions.setProperty', function () {
it('has one-way binding for properties', async function () {
this.set('setProperty', function () {
// noop
});
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
run(() => {
this.$('input[name="name"]').val('New name');
this.$('input[name="slug"]').val('new-slug');
this.$('textarea[name="description"]').val('New description');
this.$('input[name="metaTitle"]').val('New metaTitle');
this.$('textarea[name="metaDescription"]').val('New metaDescription');
});
await fillIn('input[name="name"]', 'New name');
await fillIn('input[name="slug"]', 'new-slug');
await fillIn('textarea[name="description"]', 'New description');
await fillIn('input[name="metaTitle"]', 'New metaTitle');
await fillIn('textarea[name="metaDescription"]', 'New metaDescription');
expect(this.get('tag.name'), 'tag name').to.equal('Test');
expect(this.get('tag.slug'), 'tag slug').to.equal('test');
@ -128,51 +116,35 @@ describe('Integration: Component: gh-tag-settings-form', function () {
expect(this.get('tag.metaDescription'), 'tag metaDescription').to.equal('Meta description');
});
it('triggers setProperty action on blur of all fields', function () {
let expectedProperty = '';
let expectedValue = '';
it('triggers setProperty action on blur of all fields', async function () {
let lastSeenProperty = '';
let lastSeenValue = '';
this.set('actions.setProperty', function (property, value) {
expect(property, 'property').to.equal(expectedProperty);
expect(value, 'value').to.equal(expectedValue);
this.set('setProperty', function (property, value) {
lastSeenProperty = property;
lastSeenValue = value;
});
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
let testSetProperty = async (selector, expectedProperty, expectedValue) => {
await click(selector);
await fillIn(selector, expectedValue);
await blur(selector);
expect(lastSeenProperty, 'property').to.equal(expectedProperty);
expect(lastSeenValue, 'value').to.equal(expectedValue);
};
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
expectedProperty = 'name';
expectedValue = 'new-slug';
run(() => {
this.$('input[name="name"]').val('New name');
await testSetProperty('input[name="name"]', 'name', 'New name');
await testSetProperty('input[name="slug"]', 'slug', 'new-slug');
await testSetProperty('textarea[name="description"]', 'description', 'New description');
await testSetProperty('input[name="metaTitle"]', 'metaTitle', 'New metaTitle');
await testSetProperty('textarea[name="metaDescription"]', 'metaDescription', 'New metaDescription');
});
expectedProperty = 'url';
expectedValue = 'new-slug';
run(() => {
this.$('input[name="slug"]').val('new-slug');
});
expectedProperty = 'description';
expectedValue = 'New description';
run(() => {
this.$('textarea[name="description"]').val('New description');
});
expectedProperty = 'metaTitle';
expectedValue = 'New metaTitle';
run(() => {
this.$('input[name="metaTitle"]').val('New metaTitle');
});
expectedProperty = 'metaDescription';
expectedValue = 'New metaDescription';
run(() => {
this.$('textarea[name="metaDescription"]').val('New metaDescription');
});
});
it('displays error messages for validated fields', function () {
it('displays error messages for validated fields', async function () {
let errors = this.get('tag.errors');
let hasValidated = this.get('tag.hasValidated');
@ -191,11 +163,10 @@ describe('Integration: Component: gh-tag-settings-form', function () {
errors.add('metaDescription', 'is too long');
hasValidated.push('metaDescription');
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
return wait().then(() => {
let nameFormGroup = this.$('input[name="name"]').closest('.form-group');
expect(nameFormGroup.hasClass('error'), 'name form group has error state').to.be.true;
expect(nameFormGroup.find('.response').length, 'name form group has error message').to.equal(1);
@ -215,11 +186,10 @@ describe('Integration: Component: gh-tag-settings-form', function () {
expect(metaDescriptionFormGroup.hasClass('error'), 'metaDescription form group has error state').to.be.true;
expect(metaDescriptionFormGroup.find('.response').length, 'metaDescription form group has error message').to.equal(1);
});
});
it('displays char count for text fields', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
it('displays char count for text fields', async function () {
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
let descriptionFormGroup = this.$('textarea[name="description"]').closest('.form-group');
@ -229,93 +199,79 @@ describe('Integration: Component: gh-tag-settings-form', function () {
expect(metaDescriptionFormGroup.find('.word-count').text(), 'description char count').to.equal('16');
});
it('renders SEO title preview', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
it('renders SEO title preview', async function () {
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
expect(this.$('.seo-preview-title').text(), 'displays meta title if present').to.equal('Meta Title');
expect(find('.seo-preview-title').textContent, 'displays meta title if present').to.equal('Meta Title');
run(() => {
this.set('tag.metaTitle', '');
});
expect(this.$('.seo-preview-title').text(), 'falls back to tag name without metaTitle').to.equal('Test');
expect(find('.seo-preview-title').textContent, 'falls back to tag name without metaTitle').to.equal('Test');
run(() => {
this.set('tag.name', (new Array(151).join('x')));
});
let expectedLength = 70 + '…'.length;
expect(this.$('.seo-preview-title').text().length, 'cuts title to max 70 chars').to.equal(expectedLength);
expect(find('.seo-preview-title').textContent.length, 'cuts title to max 70 chars').to.equal(expectedLength);
});
it('renders SEO URL preview', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
it('renders SEO URL preview', async function () {
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
expect(this.$('.seo-preview-link').text(), 'adds url and tag prefix').to.equal('http://localhost:2368/tag/test/');
expect(find('.seo-preview-link').textContent, 'adds url and tag prefix').to.equal('http://localhost:2368/tag/test/');
run(() => {
this.set('tag.slug', (new Array(151).join('x')));
});
let expectedLength = 70 + '…'.length;
expect(this.$('.seo-preview-link').text().length, 'cuts slug to max 70 chars').to.equal(expectedLength);
expect(find('.seo-preview-link').textContent.length, 'cuts slug to max 70 chars').to.equal(expectedLength);
});
it('renders SEO description preview', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
it('renders SEO description preview', async function () {
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
expect(this.$('.seo-preview-description').text(), 'displays meta description if present').to.equal('Meta description');
expect(find('.seo-preview-description').textContent, 'displays meta description if present').to.equal('Meta description');
run(() => {
this.set('tag.metaDescription', '');
});
expect(this.$('.seo-preview-description').text(), 'falls back to tag description without metaDescription').to.equal('Description.');
expect(find('.seo-preview-description').textContent, 'falls back to tag description without metaDescription').to.equal('Description.');
run(() => {
this.set('tag.description', (new Array(500).join('x')));
});
let expectedLength = 156 + '…'.length;
expect(this.$('.seo-preview-description').text().length, 'cuts description to max 156 chars').to.equal(expectedLength);
expect(find('.seo-preview-description').textContent.length, 'cuts description to max 156 chars').to.equal(expectedLength);
});
it('resets if a new tag is received', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
it('resets if a new tag is received', async function () {
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
run(() => {
this.$('.meta-data-button').click();
});
expect(this.$('.tag-meta-settings-pane').hasClass('settings-menu-pane-in'), 'meta data pane is shown').to.be.true;
await click('.meta-data-button');
expect(find('.tag-meta-settings-pane').classList.contains('settings-menu-pane-in'), 'meta data pane is shown').to.be.true;
run(() => {
this.set('tag', EmberObject.create({id: '2'}));
});
expect(this.$('.tag-settings-pane').hasClass('settings-menu-pane-in'), 'resets to main settings').to.be.true;
expect(find('.tag-settings-pane').classList.contains('settings-menu-pane-in'), 'resets to main settings').to.be.true;
});
it('triggers delete tag modal on delete click', function (done) {
// TODO: will time out if this isn't hit, there's probably a better
// way of testing this
this.set('actions.openModal', () => {
done();
it('triggers delete tag modal on delete click', async function () {
let openModalFired = false;
this.set('openModal', () => {
openModalFired = true;
});
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') showDeleteTagModal=(action 'openModal')}}
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty) showDeleteTagModal=(action openModal)}}
`);
await click('.settings-menu-delete-button');
expect(openModalFired).to.be.true;
});
it('shows settings.tags arrow link on mobile', async function () {
let mediaQueries = this.owner.lookup('service:media-queries');
mediaQueries.set('maxWidth600', true);
await render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action setProperty)}}
`);
run(() => {
this.$('.settings-menu-delete-button').click();
});
});
it('shows settings.tags arrow link on mobile', function () {
this.set('mediaQueries.maxWidth600', true);
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
`);
expect(this.$('.tag-settings-pane .settings-menu-header .settings-menu-header-action').length, 'settings.tags link is shown').to.equal(1);
expect(findAll('.tag-settings-pane .settings-menu-header .settings-menu-header-action').length, 'settings.tags link is shown').to.equal(1);
});
});

View file

@ -1,26 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: gh-tags-management-container', function () {
setupComponentTest('gh-tags-management-container', {
integration: true
});
it('renders', function () {
this.set('tags', []);
this.set('selectedTag', null);
this.on('enteredMobile', function () {
// noop
});
this.on('leftMobile', function () {
// noop
});
this.render(hbs`
{{#gh-tags-management-container tags=tags selectedTag=selectedTag enteredMobile="enteredMobile" leftMobile=(action "leftMobile")}}{{/gh-tags-management-container}}
`);
expect(this.$()).to.have.length(1);
});
});

View file

@ -1,135 +1,133 @@
import hbs from 'htmlbars-inline-precompile';
import wait from 'ember-test-helpers/wait';
import {click, find, render, settled} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
import {task, timeout} from 'ember-concurrency';
describe('Integration: Component: gh-task-button', function () {
setupComponentTest('gh-task-button', {
integration: true
});
setupRenderingTest();
it('renders', function () {
it('renders', async function () {
// sets button text using positional param
this.render(hbs`{{gh-task-button "Test"}}`);
expect(this.$('button')).to.exist;
expect(this.$('button')).to.contain('Test');
expect(this.$('button')).to.have.prop('disabled', false);
await render(hbs`{{gh-task-button "Test"}}`);
expect(find('button')).to.exist;
expect(find('button')).to.contain.text('Test');
expect(find('button').disabled).to.be.false;
this.render(hbs`{{gh-task-button class="testing"}}`);
expect(this.$('button')).to.have.class('testing');
await render(hbs`{{gh-task-button class="testing"}}`);
expect(find('button')).to.have.class('testing');
// default button text is "Save"
expect(this.$('button')).to.contain('Save');
expect(find('button')).to.contain.text('Save');
// passes disabled attr
this.render(hbs`{{gh-task-button disabled=true buttonText="Test"}}`);
expect(this.$('button')).to.have.prop('disabled', true);
await render(hbs`{{gh-task-button disabled=true buttonText="Test"}}`);
expect(find('button').disabled).to.be.true;
// allows button text to be set via hash param
expect(this.$('button')).to.contain('Test');
expect(find('button')).to.contain.text('Test');
// passes type attr
this.render(hbs`{{gh-task-button type="submit"}}`);
expect(this.$('button')).to.have.attr('type', 'submit');
await render(hbs`{{gh-task-button type="submit"}}`);
expect(find('button')).to.have.attr('type', 'submit');
// passes tabindex attr
this.render(hbs`{{gh-task-button tabindex="-1"}}`);
expect(this.$('button')).to.have.attr('tabindex', '-1');
await render(hbs`{{gh-task-button tabindex="-1"}}`);
expect(find('button')).to.have.attr('tabindex', '-1');
});
it('shows spinner whilst running', function () {
it('shows spinner whilst running', async function () {
this.set('myTask', task(function* () {
yield timeout(50);
}));
this.render(hbs`{{gh-task-button task=myTask}}`);
await render(hbs`{{gh-task-button task=myTask}}`);
this.get('myTask').perform();
run.later(this, function () {
expect(this.$('button')).to.have.descendants('svg');
expect(find('button')).to.have.descendants('svg');
}, 20);
return wait();
await settled();
});
it('shows running text when passed whilst running', function () {
it('shows running text when passed whilst running', async function () {
this.set('myTask', task(function* () {
yield timeout(50);
}));
this.render(hbs`{{gh-task-button task=myTask runningText="Running"}}`);
await render(hbs`{{gh-task-button task=myTask runningText="Running"}}`);
this.get('myTask').perform();
run.later(this, function () {
expect(this.$('button')).to.have.descendants('svg');
expect(this.$('button')).to.contain('Running');
expect(find('button')).to.have.descendants('svg');
expect(find('button')).to.contain.text('Running');
}, 20);
return wait();
await settled();
});
it('appears disabled whilst running', function () {
it('appears disabled whilst running', async function () {
this.set('myTask', task(function* () {
yield timeout(50);
}));
this.render(hbs`{{gh-task-button task=myTask}}`);
expect(this.$('button'), 'initial class').to.not.have.class('appear-disabled');
await render(hbs`{{gh-task-button task=myTask}}`);
expect(find('button'), 'initial class').to.not.have.class('appear-disabled');
this.get('myTask').perform();
run.later(this, function () {
expect(this.$('button'), 'running class').to.have.class('appear-disabled');
expect(find('button'), 'running class').to.have.class('appear-disabled');
}, 20);
run.later(this, function () {
expect(this.$('button'), 'ended class').to.not.have.class('appear-disabled');
expect(find('button'), 'ended class').to.not.have.class('appear-disabled');
}, 100);
return wait();
await settled();
});
it('shows success on success', function () {
it('shows success on success', async function () {
this.set('myTask', task(function* () {
yield timeout(50);
return true;
}));
this.render(hbs`{{gh-task-button task=myTask}}`);
await render(hbs`{{gh-task-button task=myTask}}`);
this.get('myTask').perform();
run.later(this, function () {
expect(this.$('button')).to.have.class('gh-btn-green');
expect(this.$('button')).to.contain('Saved');
expect(find('button')).to.have.class('gh-btn-green');
expect(find('button')).to.contain.text('Saved');
}, 100);
return wait();
await settled();
});
it('assigns specified success class on success', function () {
it('assigns specified success class on success', async function () {
this.set('myTask', task(function* () {
yield timeout(50);
return true;
}));
this.render(hbs`{{gh-task-button task=myTask successClass="im-a-success"}}`);
await render(hbs`{{gh-task-button task=myTask successClass="im-a-success"}}`);
this.get('myTask').perform();
run.later(this, function () {
expect(this.$('button')).to.not.have.class('gh-btn-green');
expect(this.$('button')).to.have.class('im-a-success');
expect(this.$('button')).to.contain('Saved');
expect(find('button')).to.not.have.class('gh-btn-green');
expect(find('button')).to.have.class('im-a-success');
expect(find('button')).to.contain.text('Saved');
}, 100);
return wait();
await settled();
});
it('shows failure when task errors', function () {
it('shows failure when task errors', async function () {
this.set('myTask', task(function* () {
try {
yield timeout(50);
@ -139,56 +137,56 @@ describe('Integration: Component: gh-task-button', function () {
}
}));
this.render(hbs`{{gh-task-button task=myTask}}`);
await render(hbs`{{gh-task-button task=myTask}}`);
this.get('myTask').perform();
run.later(this, function () {
expect(this.$('button')).to.have.class('gh-btn-red');
expect(this.$('button')).to.contain('Retry');
expect(find('button')).to.have.class('gh-btn-red');
expect(find('button')).to.contain.text('Retry');
}, 100);
return wait();
await settled();
});
it('shows failure on falsy response', function () {
it('shows failure on falsy response', async function () {
this.set('myTask', task(function* () {
yield timeout(50);
return false;
}));
this.render(hbs`{{gh-task-button task=myTask}}`);
await render(hbs`{{gh-task-button task=myTask}}`);
this.get('myTask').perform();
run.later(this, function () {
expect(this.$('button')).to.have.class('gh-btn-red');
expect(this.$('button')).to.contain('Retry');
expect(find('button')).to.have.class('gh-btn-red');
expect(find('button')).to.contain.text('Retry');
}, 100);
return wait();
await settled();
});
it('assigns specified failure class on failure', function () {
it('assigns specified failure class on failure', async function () {
this.set('myTask', task(function* () {
yield timeout(50);
return false;
}));
this.render(hbs`{{gh-task-button task=myTask failureClass="im-a-failure"}}`);
await render(hbs`{{gh-task-button task=myTask failureClass="im-a-failure"}}`);
this.get('myTask').perform();
run.later(this, function () {
expect(this.$('button')).to.not.have.class('gh-btn-red');
expect(this.$('button')).to.have.class('im-a-failure');
expect(this.$('button')).to.contain('Retry');
expect(find('button')).to.not.have.class('gh-btn-red');
expect(find('button')).to.have.class('im-a-failure');
expect(find('button')).to.contain.text('Retry');
}, 100);
return wait();
await settled();
});
it('performs task on click', function () {
it('performs task on click', async function () {
let taskCount = 0;
this.set('myTask', task(function* () {
@ -196,43 +194,41 @@ describe('Integration: Component: gh-task-button', function () {
taskCount = taskCount + 1;
}));
this.render(hbs`{{gh-task-button task=myTask}}`);
this.$('button').click();
await render(hbs`{{gh-task-button task=myTask}}`);
await click('button');
return wait().then(() => {
await settled().then(() => {
expect(taskCount, 'taskCount').to.equal(1);
});
});
it.skip('keeps button size when showing spinner', function () {
it.skip('keeps button size when showing spinner', async function () {
this.set('myTask', task(function* () {
yield timeout(50);
}));
this.render(hbs`{{gh-task-button task=myTask}}`);
let width = this.$('button').width();
let height = this.$('button').height();
expect(this.$('button')).to.not.have.attr('style');
await render(hbs`{{gh-task-button task=myTask}}`);
let width = find('button').clientWidth;
let height = find('button').clientHeight;
expect(find('button')).to.not.have.attr('style');
this.get('myTask').perform();
run.later(this, function () {
// we can't test exact width/height because Chrome/Firefox use different rounding methods
// expect(this.$('button')).to.have.attr('style', `width: ${width}px; height: ${height}px;`);
// expect(find('button')).to.have.attr('style', `width: ${width}px; height: ${height}px;`);
let [widthInt] = width.toString().split('.');
let [heightInt] = height.toString().split('.');
expect(this.$('button').attr('style')).to.have.string(`width: ${widthInt}`);
expect(this.$('button').attr('style')).to.have.string(`height: ${heightInt}`);
expect(find('button')).to.have.attr('style', `width: ${widthInt}`);
expect(find('button')).to.have.attr('style', `height: ${heightInt}`);
}, 20);
run.later(this, function () {
// chai-jquery test doesn't work because Firefox outputs blank string
// expect(this.$('button')).to.not.have.attr('style');
expect(this.$('button').attr('style')).to.be.empty;
expect(find('button').getAttribute('style')).to.be.empty;
}, 100);
return wait();
await settled();
});
});

View file

@ -1,17 +1,14 @@
import $ from 'jquery';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import {click, find, findAll, render} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-theme-table', function () {
setupComponentTest('gh-theme-table', {
integration: true
});
setupRenderingTest();
it('renders', function () {
it('renders', async function () {
this.set('themes', [
{name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
@ -20,17 +17,17 @@ describe('Integration: Component: gh-theme-table', function () {
]);
this.set('actionHandler', sinon.spy());
this.render(hbs`{{gh-theme-table
await render(hbs`{{gh-theme-table
themes=themes
activateTheme=(action actionHandler)
downloadTheme=(action actionHandler)
deleteTheme=(action actionHandler)
}}`);
expect(this.$('[data-test-themes-list]').length, 'themes list is present').to.equal(1);
expect(this.$('[data-test-theme-id]').length, 'number of rows').to.equal(4);
expect(findAll('[data-test-themes-list]').length, 'themes list is present').to.equal(1);
expect(findAll('[data-test-theme-id]').length, 'number of rows').to.equal(4);
let packageNames = this.$('[data-test-theme-title]').map((i, name) => $(name).text().trim()).toArray();
let packageNames = findAll('[data-test-theme-title]').map(name => name.textContent.trim());
expect(
packageNames,
@ -43,42 +40,42 @@ describe('Integration: Component: gh-theme-table', function () {
]);
expect(
this.$('[data-test-theme-active="true"]').find('[data-test-theme-title]').text().trim(),
find('[data-test-theme-active="true"]').querySelector('[data-test-theme-title]'),
'active theme is highlighted'
).to.equal('Daring');
).to.have.trimmed.text('Daring');
expect(
this.$('[data-test-theme-activate-button]').length === 3,
findAll('[data-test-theme-activate-button]').length,
'non-active themes have an activate link'
).to.be.true;
).to.equal(3);
expect(
this.$('[data-test-theme-active="true"]').find('[data-test-theme-activate-button]').length === 0,
find('[data-test-theme-active="true"]').querySelector('[data-test-theme-activate-button]'),
'active theme doesn\'t have an activate link'
).to.be.true;
).to.not.exist;
expect(
this.$('[data-test-theme-download-button]').length,
findAll('[data-test-theme-download-button]').length,
'all themes have a download link'
).to.equal(4);
expect(
this.$('[data-test-theme-id="foo"]').find('[data-test-theme-delete-button]').length === 1,
find('[data-test-theme-id="foo"]').querySelector('[data-test-theme-delete-button]'),
'non-active, non-casper theme has delete link'
).to.be.true;
).to.exist;
expect(
this.$('[data-test-theme-id="casper"]').find('[data-test-theme-delete-button]').length === 0,
find('[data-test-theme-id="casper"]').querySelector('[data-test-theme-delete-button]'),
'casper doesn\'t have delete link'
).to.be.true;
).to.not.exist;
expect(
this.$('[data-test-theme-active="true"]').find('[data-test-theme-delete-button]').length === 0,
find('[data-test-theme-active="true"]').querySelector('[data-test-theme-delete-button]'),
'active theme doesn\'t have delete link'
).to.be.true;
).to.not.exist;
});
it('delete link triggers passed in action', function () {
it('delete link triggers passed in action', async function () {
let deleteAction = sinon.spy();
let actionHandler = sinon.spy();
@ -89,22 +86,20 @@ describe('Integration: Component: gh-theme-table', function () {
this.set('deleteAction', deleteAction);
this.set('actionHandler', actionHandler);
this.render(hbs`{{gh-theme-table
await render(hbs`{{gh-theme-table
themes=themes
activateTheme=(action actionHandler)
downloadTheme=(action actionHandler)
deleteTheme=(action deleteAction)
}}`);
run(() => {
this.$('[data-test-theme-id="Bar"] [data-test-theme-delete-button]').click();
});
await click('[data-test-theme-id="Bar"] [data-test-theme-delete-button]');
expect(deleteAction.calledOnce).to.be.true;
expect(deleteAction.firstCall.args[0].name).to.equal('Bar');
});
it('download link triggers passed in action', function () {
it('download link triggers passed in action', async function () {
let downloadAction = sinon.spy();
let actionHandler = sinon.spy();
@ -115,22 +110,20 @@ describe('Integration: Component: gh-theme-table', function () {
this.set('downloadAction', downloadAction);
this.set('actionHandler', actionHandler);
this.render(hbs`{{gh-theme-table
await render(hbs`{{gh-theme-table
themes=themes
activateTheme=(action actionHandler)
downloadTheme=(action downloadAction)
deleteTheme=(action actionHandler)
}}`);
run(() => {
this.$('[data-test-theme-id="Foo"] [data-test-theme-download-button]').click();
});
await click('[data-test-theme-id="Foo"] [data-test-theme-download-button]');
expect(downloadAction.calledOnce).to.be.true;
expect(downloadAction.firstCall.args[0].name).to.equal('Foo');
});
it('activate link triggers passed in action', function () {
it('activate link triggers passed in action', async function () {
let activateAction = sinon.spy();
let actionHandler = sinon.spy();
@ -141,22 +134,20 @@ describe('Integration: Component: gh-theme-table', function () {
this.set('activateAction', activateAction);
this.set('actionHandler', actionHandler);
this.render(hbs`{{gh-theme-table
await render(hbs`{{gh-theme-table
themes=themes
activateTheme=(action activateAction)
downloadTheme=(action actionHandler)
deleteTheme=(action actionHandler)
}}`);
run(() => {
this.$('[data-test-theme-id="Bar"] [data-test-theme-activate-button]').click();
});
await click('[data-test-theme-id="Bar"] [data-test-theme-activate-button]');
expect(activateAction.calledOnce).to.be.true;
expect(activateAction.firstCall.args[0].name).to.equal('Bar');
});
it('displays folder names if there are duplicate package names', function () {
it('displays folder names if there are duplicate package names', async function () {
this.set('themes', [
{name: 'daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
{name: 'daring-0.1.5', package: {name: 'Daring', version: '0.1.4'}},
@ -167,14 +158,14 @@ describe('Integration: Component: gh-theme-table', function () {
]);
this.set('actionHandler', sinon.spy());
this.render(hbs`{{gh-theme-table
await render(hbs`{{gh-theme-table
themes=themes
activateTheme=(action actionHandler)
downloadTheme=(action actionHandler)
deleteTheme=(action actionHandler)
}}`);
let packageNames = this.$('[data-test-theme-title]').map((i, name) => $(name).text().trim()).toArray();
let packageNames = findAll('[data-test-theme-title]').map(name => name.textContent.trim());
expect(
packageNames,

View file

@ -1,15 +1,12 @@
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import wait from 'ember-test-helpers/wait';
import {blur, fillIn, find, findAll, render} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-timezone-select', function () {
setupComponentTest('gh-timezone-select', {
integration: true
});
setupRenderingTest();
beforeEach(function () {
this.set('availableTimezones', [
@ -20,50 +17,46 @@ describe('Integration: Component: gh-timezone-select', function () {
this.set('activeTimezone', 'Etc/UTC');
});
it('renders', function () {
this.render(hbs`{{gh-timezone-select
it('renders', async function () {
await render(hbs`{{gh-timezone-select
availableTimezones=availableTimezones
activeTimezone=activeTimezone}}`);
expect(this.$(), 'top-level elements').to.have.length(1);
expect(this.$('option'), 'number of options').to.have.length(3);
expect(this.$('select').val(), 'selected option value').to.equal('Etc/UTC');
expect(this.element, 'top-level elements').to.exist;
expect(findAll('option'), 'number of options').to.have.length(3);
expect(find('select').value, 'selected option value').to.equal('Etc/UTC');
});
it('handles an unknown timezone', function () {
it('handles an unknown timezone', async function () {
this.set('activeTimezone', 'Europe/London');
this.render(hbs`{{gh-timezone-select
await render(hbs`{{gh-timezone-select
availableTimezones=availableTimezones
activeTimezone=activeTimezone}}`);
// we have an additional blank option at the top
expect(this.$('option'), 'number of options').to.have.length(4);
expect(findAll('option'), 'number of options').to.have.length(4);
// blank option is selected
expect(this.$('select').val(), 'selected option value').to.equal('');
expect(find('select').value, 'selected option value').to.equal('');
// we indicate the manual override
expect(this.$('p').text()).to.match(/Your timezone has been automatically set to Europe\/London/);
expect(find('p').textContent).to.match(/Your timezone has been automatically set to Europe\/London/);
});
it('triggers update action on change', function (done) {
it('triggers update action on change', async function () {
let update = sinon.spy();
this.set('update', update);
this.render(hbs`{{gh-timezone-select
await render(hbs`{{gh-timezone-select
availableTimezones=availableTimezones
activeTimezone=activeTimezone
update=(action update)}}`);
run(() => {
this.$('select').val('Pacific/Pago_Pago').change();
});
await fillIn('select', 'Pacific/Pago_Pago');
await blur('select');
wait().then(() => {
expect(update.calledOnce, 'update was called once').to.be.true;
expect(update.firstCall.args[0].name, 'update was passed new timezone')
.to.equal('Pacific/Pago_Pago');
done();
});
});
// TODO: mock clock service, fake the time, test we have the correct

View file

@ -1,66 +1,60 @@
import hbs from 'htmlbars-inline-precompile';
import {blur, find, render} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-trim-focus-input', function () {
setupComponentTest('gh-trim-focus-input', {
integration: true
});
setupRenderingTest();
it('trims value on focusOut', function () {
it('trims value on focusOut', async function () {
this.set('text', 'some random stuff ');
this.render(hbs`{{gh-trim-focus-input value=(readonly text) input=(action (mut text) value="target.value")}}`);
await render(hbs`{{gh-trim-focus-input value=(readonly text) input=(action (mut text) value="target.value")}}`);
run(() => {
this.$('.gh-input').trigger('focusout');
});
await blur('input');
expect(this.get('text')).to.equal('some random stuff');
});
it('trims value on focusOut before calling custom focus-out', function () {
it('trims value on focusOut before calling custom focus-out', async function () {
this.set('text', 'some random stuff ');
this.set('customFocusOut', function (value) {
expect(this.$('.gh-input').val(), 'input value').to.equal('some random stuff');
expect(find('.gh-input').value, 'input value').to.equal('some random stuff');
expect(value, 'value').to.equal('some random stuff');
});
this.render(hbs`{{gh-trim-focus-input
await render(hbs`{{gh-trim-focus-input
value=(readonly text)
input=(action (mut text) value="target.value")
focus-out=(action customFocusOut)
}}`);
run(() => {
this.$('.gh-input').trigger('focusout');
});
await blur('input');
expect(this.get('text')).to.equal('some random stuff');
});
it('does not have the autofocus attribute if not set to focus', function () {
it('does not have the autofocus attribute if not set to focus', async function () {
this.set('text', 'some text');
this.render(hbs`{{gh-trim-focus-input value=(readonly text) shouldFocus=false}}`);
expect(this.$('.gh-input').attr('autofocus')).to.not.be.ok;
await render(hbs`{{gh-trim-focus-input value=(readonly text) shouldFocus=false}}`);
expect(find('input').autofocus).to.not.be.ok;
});
it('has the autofocus attribute if set to focus', function () {
it('has the autofocus attribute if set to focus', async function () {
this.set('text', 'some text');
this.render(hbs`{{gh-trim-focus-input value=(readonly text) shouldFocus=true}}`);
expect(this.$('.gh-input').attr('autofocus')).to.be.ok;
await render(hbs`{{gh-trim-focus-input value=(readonly text) shouldFocus=true}}`);
expect(find('input').autofocus).to.be.ok;
});
it('handles undefined values', function () {
it('handles undefined values', async function () {
this.set('text', undefined);
this.render(hbs`{{gh-trim-focus-input value=(readonly text) shouldFocus=true}}`);
expect(this.$('.gh-input').attr('autofocus')).to.be.ok;
await render(hbs`{{gh-trim-focus-input value=(readonly text) shouldFocus=true}}`);
expect(find('input').autofocus).to.be.ok;
});
it('handles non-string values', function () {
it('handles non-string values', async function () {
this.set('text', 10);
this.render(hbs`{{gh-trim-focus-input value=(readonly text) shouldFocus=true}}`);
expect(this.$('.gh-input').val()).to.equal('10');
await render(hbs`{{gh-trim-focus-input value=(readonly text) shouldFocus=true}}`);
expect(find('input').value).to.equal('10');
});
});

View file

@ -1,13 +1,11 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {find} from 'ember-native-dom-helpers';
import {setupComponentTest} from 'ember-mocha';
import {find, render} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-unsplash-photo', function () {
setupComponentTest('gh-unsplash-photo', {
integration: true
});
setupRenderingTest();
beforeEach(function () {
// NOTE: images.unsplash.com replaced with example.com to ensure we aren't
@ -70,16 +68,16 @@ describe('Integration: Component: gh-unsplash-photo', function () {
});
});
it('sets background-color style', function () {
this.render(hbs`{{gh-unsplash-photo photo=photo}}`);
it('sets background-color style', async function () {
await render(hbs`{{gh-unsplash-photo photo=photo}}`);
expect(
find('[data-test-unsplash-photo-container]').attributes.style.value
).to.have.string('background-color: #A8A99B');
});
it('sets padding-bottom style', function () {
this.render(hbs`{{gh-unsplash-photo photo=photo}}`);
it('sets padding-bottom style', async function () {
await render(hbs`{{gh-unsplash-photo photo=photo}}`);
// don't check full padding-bottom value as it will likely vary across
// browsers
@ -88,16 +86,16 @@ describe('Integration: Component: gh-unsplash-photo', function () {
).to.have.string('padding-bottom: 66.66');
});
it('uses correct image size url', function () {
this.render(hbs`{{gh-unsplash-photo photo=photo}}`);
it('uses correct image size url', async function () {
await render(hbs`{{gh-unsplash-photo photo=photo}}`);
expect(
find('[data-test-unsplash-photo-image]').attributes.src.value
).to.have.string('&w=1200');
});
it('calculates image width/height', function () {
this.render(hbs`{{gh-unsplash-photo photo=photo}}`);
it('calculates image width/height', async function () {
await render(hbs`{{gh-unsplash-photo photo=photo}}`);
expect(
find('[data-test-unsplash-photo-image]').attributes.width.value

View file

@ -1,24 +1,23 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {render} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
describe('Integration: Component: gh-unsplash', function () {
setupComponentTest('gh-unsplash', {
integration: true
});
setupRenderingTest();
it('renders', function () {
it('renders', async function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// await render(hbs`
// {{#gh-unsplash}}
// template content
// {{/gh-unsplash}}
// `);
this.render(hbs`{{gh-unsplash}}`);
await render(hbs`{{gh-unsplash}}`);
expect(this.$()).to.have.length(1);
});

View file

@ -1,13 +1,12 @@
import Pretender from 'pretender';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import wait from 'ember-test-helpers/wait';
import {click, find, findAll} from 'ember-native-dom-helpers';
import {click, find, findAll, render, settled} from '@ember/test-helpers';
import {createFile} from '../../helpers/file-upload';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupComponentTest} from 'ember-mocha';
import {setupRenderingTest} from 'ember-mocha';
const stubSuccessfulUpload = function (server, delay = 0) {
server.post('/ghost/api/v2/admin/uploads/', function () {
@ -27,9 +26,7 @@ const stubFailedUpload = function (server, code, error, delay = 0) {
};
describe('Integration: Component: gh-uploader', function () {
setupComponentTest('gh-uploader', {
integration: true
});
setupRenderingTest();
let server;
@ -47,10 +44,10 @@ describe('Integration: Component: gh-uploader', function () {
});
it('triggers uploads when `files` is set', async function () {
this.render(hbs`{{#gh-uploader files=files}}{{/gh-uploader}}`);
await render(hbs`{{#gh-uploader files=files}}{{/gh-uploader}}`);
this.set('files', [createFile()]);
await wait();
await settled();
let [lastRequest] = server.handledRequests;
expect(server.handledRequests.length).to.equal(1);
@ -62,10 +59,10 @@ describe('Integration: Component: gh-uploader', function () {
});
it('triggers multiple uploads', async function () {
this.render(hbs`{{#gh-uploader files=files}}{{/gh-uploader}}`);
await render(hbs`{{#gh-uploader files=files}}{{/gh-uploader}}`);
this.set('files', [createFile(), createFile()]);
await wait();
await settled();
expect(server.handledRequests.length).to.equal(2);
});
@ -73,9 +70,9 @@ describe('Integration: Component: gh-uploader', function () {
it('triggers onStart when upload starts', async function () {
this.set('uploadStarted', sinon.spy());
this.render(hbs`{{#gh-uploader files=files onStart=(action uploadStarted)}}{{/gh-uploader}}`);
await render(hbs`{{#gh-uploader files=files onStart=(action uploadStarted)}}{{/gh-uploader}}`);
this.set('files', [createFile(), createFile()]);
await wait();
await settled();
expect(this.get('uploadStarted').calledOnce).to.be.true;
});
@ -83,9 +80,9 @@ describe('Integration: Component: gh-uploader', function () {
it('triggers onUploadSuccess when a file uploads', async function () {
this.set('fileUploaded', sinon.spy());
this.render(hbs`{{#gh-uploader files=files onUploadSuccess=(action fileUploaded)}}{{/gh-uploader}}`);
await render(hbs`{{#gh-uploader files=files onUploadSuccess=(action fileUploaded)}}{{/gh-uploader}}`);
this.set('files', [createFile(['test'], {name: 'file1.png'}), createFile()]);
await wait();
await settled();
// triggered for each file
expect(this.get('fileUploaded').calledTwice).to.be.true;
@ -99,12 +96,12 @@ describe('Integration: Component: gh-uploader', function () {
it('triggers onComplete when all files uploaded', async function () {
this.set('uploadsFinished', sinon.spy());
this.render(hbs`{{#gh-uploader files=files onComplete=(action uploadsFinished)}}{{/gh-uploader}}`);
await render(hbs`{{#gh-uploader files=files onComplete=(action uploadsFinished)}}{{/gh-uploader}}`);
this.set('files', [
createFile(['test'], {name: 'file1.png'}),
createFile(['test'], {name: 'file2.png'})
]);
await wait();
await settled();
expect(this.get('uploadsFinished').calledOnce).to.be.true;
@ -120,17 +117,17 @@ describe('Integration: Component: gh-uploader', function () {
it('onComplete only passes results for last upload', async function () {
this.set('uploadsFinished', sinon.spy());
this.render(hbs`{{#gh-uploader files=files onComplete=(action uploadsFinished)}}{{/gh-uploader}}`);
await render(hbs`{{#gh-uploader files=files onComplete=(action uploadsFinished)}}{{/gh-uploader}}`);
this.set('files', [
createFile(['test'], {name: 'file1.png'})
]);
await wait();
await settled();
this.set('files', [
createFile(['test'], {name: 'file2.png'})
]);
await wait();
await settled();
let [results] = this.get('uploadsFinished').getCall(1).args;
expect(results.length).to.equal(1);
@ -148,12 +145,12 @@ describe('Integration: Component: gh-uploader', function () {
this.set('uploadsFinished', sinon.spy());
this.render(hbs`{{#gh-uploader files=files onComplete=(action uploadsFinished)}}{{/gh-uploader}}`);
await render(hbs`{{#gh-uploader files=files onComplete=(action uploadsFinished)}}{{/gh-uploader}}`);
this.set('files', [
createFile(['test'], {name: 'file1.png'}), // large - finishes last
createFile(['test'], {name: 'file2.png'}) // small - finishes first
]);
await wait();
await settled();
let [results] = this.get('uploadsFinished').getCall(0).args;
expect(results.length).to.equal(2);
@ -164,7 +161,7 @@ describe('Integration: Component: gh-uploader', function () {
let errorSpy = sinon.spy(console, 'error');
stubSuccessfulUpload(server, 100);
this.render(hbs`{{#gh-uploader files=files}}{{/gh-uploader}}`);
await render(hbs`{{#gh-uploader files=files}}{{/gh-uploader}}`);
this.set('files', [createFile()]);
// logs error because upload is in progress
@ -177,7 +174,7 @@ describe('Integration: Component: gh-uploader', function () {
this.set('files', [createFile()]);
}, 200);
await wait();
await settled();
expect(server.handledRequests.length).to.equal(2);
expect(errorSpy.calledOnce).to.be.true;
@ -187,7 +184,7 @@ describe('Integration: Component: gh-uploader', function () {
it('yields isUploading whilst upload is in progress', async function () {
stubSuccessfulUpload(server, 200);
this.render(hbs`
await render(hbs`
{{#gh-uploader files=files as |uploader|}}
{{#if uploader.isUploading}}
<div class="is-uploading-test"></div>
@ -200,7 +197,7 @@ describe('Integration: Component: gh-uploader', function () {
expect(find('.is-uploading-test')).to.exist;
}, 100);
await wait();
await settled();
expect(find('.is-uploading-test')).to.not.exist;
});
@ -208,7 +205,7 @@ describe('Integration: Component: gh-uploader', function () {
it('yields progressBar component with total upload progress', async function () {
stubSuccessfulUpload(server, 200);
this.render(hbs`
await render(hbs`
{{#gh-uploader files=files as |uploader|}}
{{uploader.progressBar}}
{{/gh-uploader}}`);
@ -222,14 +219,14 @@ describe('Integration: Component: gh-uploader', function () {
expect(progressWidth).to.be.below(100);
}, 100);
await wait();
await settled();
let progressWidth = parseInt(find('[data-test-progress-bar]').style.width);
expect(progressWidth).to.equal(100);
});
it('yields files property', function () {
this.render(hbs`
it('yields files property', async function () {
await render(hbs`
{{#gh-uploader files=files as |uploader|}}
{{#each uploader.files as |file|}}
<div class="file">{{file.name}}</div>
@ -250,7 +247,7 @@ describe('Integration: Component: gh-uploader', function () {
this.set('cancelled', sinon.spy());
this.set('complete', sinon.spy());
this.render(hbs`
await render(hbs`
{{#gh-uploader files=files onCancel=(action cancelled) as |uploader|}}
<button class="cancel-button" {{action uploader.cancel}}>Cancel</button>
{{/gh-uploader}}`);
@ -261,7 +258,7 @@ describe('Integration: Component: gh-uploader', function () {
click('.cancel-button');
}, 50);
await wait();
await settled();
expect(this.get('cancelled').calledOnce, 'onCancel triggered').to.be.true;
expect(this.get('complete').notCalled, 'onComplete triggered').to.be.true;
@ -272,18 +269,18 @@ describe('Integration: Component: gh-uploader', function () {
return [200, {'Content-Type': 'application/json'}, '"/content/images/test.png"'];
});
this.render(hbs`{{#gh-uploader files=files uploadUrl="/images/"}}{{/gh-uploader}}`);
await render(hbs`{{#gh-uploader files=files uploadUrl="/images/"}}{{/gh-uploader}}`);
this.set('files', [createFile()]);
await wait();
await settled();
let [lastRequest] = server.handledRequests;
expect(lastRequest.url).to.equal('/ghost/api/v2/admin/images/');
});
it('passes supplied paramName in request', async function () {
this.render(hbs`{{#gh-uploader files=files paramName="testupload"}}{{/gh-uploader}}`);
await render(hbs`{{#gh-uploader files=files paramName="testupload"}}{{/gh-uploader}}`);
this.set('files', [createFile()]);
await wait();
await settled();
let [lastRequest] = server.handledRequests;
// requestBody is a FormData object
@ -297,11 +294,11 @@ describe('Integration: Component: gh-uploader', function () {
it('validates file extensions by default', async function () {
this.set('onFailed', sinon.spy());
this.render(hbs`
await render(hbs`
{{#gh-uploader files=files extensions="jpg,jpeg" onFailed=(action onFailed)}}{{/gh-uploader}}
`);
this.set('files', [createFile(['test'], {name: 'test.png'})]);
await wait();
await settled();
let [onFailedResult] = this.get('onFailed').firstCall.args;
expect(onFailedResult.length).to.equal(1);
@ -315,11 +312,11 @@ describe('Integration: Component: gh-uploader', function () {
});
this.set('onFailed', sinon.spy());
this.render(hbs`
await render(hbs`
{{#gh-uploader files=files validate=(action validate) onFailed=(action onFailed)}}{{/gh-uploader}}
`);
this.set('files', [createFile(['test'], {name: 'test.png'})]);
await wait();
await settled();
let [onFailedResult] = this.get('onFailed').firstCall.args;
expect(onFailedResult.length).to.equal(1);
@ -328,7 +325,7 @@ describe('Integration: Component: gh-uploader', function () {
});
it('yields errors when validation fails', async function () {
this.render(hbs`
await render(hbs`
{{#gh-uploader files=files extensions="jpg,jpeg" as |uploader|}}
{{#each uploader.errors as |error|}}
<div class="error-fileName">{{error.fileName}}</div>
@ -337,7 +334,7 @@ describe('Integration: Component: gh-uploader', function () {
{{/gh-uploader}}
`);
this.set('files', [createFile(['test'], {name: 'test.png'})]);
await wait();
await settled();
expect(find('.error-fileName').textContent).to.equal('test.png');
expect(find('.error-message').textContent).to.match(/not supported/);
@ -353,7 +350,7 @@ describe('Integration: Component: gh-uploader', function () {
this.set('uploadFailed', sinon.spy());
this.set('uploadComplete', sinon.spy());
this.render(hbs`
await render(hbs`
{{#gh-uploader
files=files
onFailed=(action uploadFailed)
@ -364,7 +361,7 @@ describe('Integration: Component: gh-uploader', function () {
createFile(['test'], {name: 'file1.png'}),
createFile(['test'], {name: 'file2.png'})
]);
await wait();
await settled();
expect(this.get('uploadFailed').calledOnce).to.be.true;
expect(this.get('uploadComplete').calledOnce).to.be.true;
@ -378,7 +375,7 @@ describe('Integration: Component: gh-uploader', function () {
it('triggers onUploadFailure when each upload fails', async function () {
this.set('uploadFail', sinon.spy());
this.render(hbs`
await render(hbs`
{{#gh-uploader
files=files
onUploadFailure=(action uploadFail)}}
@ -388,7 +385,7 @@ describe('Integration: Component: gh-uploader', function () {
createFile(['test'], {name: 'file1.png'}),
createFile(['test'], {name: 'file2.png'})
]);
await wait();
await settled();
expect(this.get('uploadFail').calledTwice).to.be.true;
@ -402,7 +399,7 @@ describe('Integration: Component: gh-uploader', function () {
});
it('yields errors when uploads fail', async function () {
this.render(hbs`
await render(hbs`
{{#gh-uploader files=files as |uploader|}}
{{#each uploader.errors as |error|}}
<div class="error-fileName">{{error.fileName}}</div>
@ -411,7 +408,7 @@ describe('Integration: Component: gh-uploader', function () {
{{/gh-uploader}}
`);
this.set('files', [createFile(['test'], {name: 'test.png'})]);
await wait();
await settled();
expect(find('.error-fileName').textContent).to.equal('test.png');
expect(find('.error-message').textContent).to.equal('Error: No upload for you');

View file

@ -1,17 +1,15 @@
import DS from 'ember-data';
import EmberObject from '@ember/object';
import hbs from 'htmlbars-inline-precompile';
import wait from 'ember-test-helpers/wait';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
import {find, render} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
const {Errors} = DS;
describe('Integration: Component: gh-validation-status-container', function () {
setupComponentTest('gh-validation-status-container', {
integration: true
});
setupRenderingTest();
beforeEach(function () {
let testObject = EmberObject.create();
@ -22,60 +20,52 @@ describe('Integration: Component: gh-validation-status-container', function () {
this.set('testObject', testObject);
});
it('has no success/error class by default', function () {
this.render(hbs`
it('has no success/error class by default', async function () {
await render(hbs`
{{#gh-validation-status-container class="gh-test" property="name" errors=testObject.errors hasValidated=testObject.hasValidated}}
{{/gh-validation-status-container}}
`);
return wait().then(() => {
expect(this.$('.gh-test')).to.have.length(1);
expect(this.$('.gh-test').hasClass('success')).to.be.false;
expect(this.$('.gh-test').hasClass('error')).to.be.false;
});
expect(find('.gh-test')).to.exist;
expect(find('.gh-test')).to.not.have.class('success');
expect(find('.gh-test')).to.not.have.class('error');
});
it('has success class when valid', function () {
it('has success class when valid', async function () {
this.get('testObject.hasValidated').push('name');
this.render(hbs`
await render(hbs`
{{#gh-validation-status-container class="gh-test" property="name" errors=testObject.errors hasValidated=testObject.hasValidated}}
{{/gh-validation-status-container}}
`);
return wait().then(() => {
expect(this.$('.gh-test')).to.have.length(1);
expect(this.$('.gh-test').hasClass('success')).to.be.true;
expect(this.$('.gh-test').hasClass('error')).to.be.false;
});
expect(find('.gh-test')).to.exist;
expect(find('.gh-test')).to.have.class('success');
expect(find('.gh-test')).to.not.have.class('error');
});
it('has error class when invalid', function () {
it('has error class when invalid', async function () {
this.get('testObject.hasValidated').push('name');
this.get('testObject.errors').add('name', 'has error');
this.render(hbs`
await render(hbs`
{{#gh-validation-status-container class="gh-test" property="name" errors=testObject.errors hasValidated=testObject.hasValidated}}
{{/gh-validation-status-container}}
`);
return wait().then(() => {
expect(this.$('.gh-test')).to.have.length(1);
expect(this.$('.gh-test').hasClass('success')).to.be.false;
expect(this.$('.gh-test').hasClass('error')).to.be.true;
});
expect(find('.gh-test')).to.exist;
expect(find('.gh-test')).to.not.have.class('success');
expect(find('.gh-test')).to.have.class('error');
});
it('still renders if hasValidated is undefined', function () {
it('still renders if hasValidated is undefined', async function () {
this.set('testObject.hasValidated', undefined);
this.render(hbs`
await render(hbs`
{{#gh-validation-status-container class="gh-test" property="name" errors=testObject.errors hasValidated=testObject.hasValidated}}
{{/gh-validation-status-container}}
`);
return wait().then(() => {
expect(this.$('.gh-test')).to.have.length(1);
});
expect(find('.gh-test')).to.exist;
});
});

Some files were not shown because too many files have changed in this diff Show more