diff --git a/ghost/admin/app/components/gh-member-avatar.js b/ghost/admin/app/components/gh-member-avatar.js index 0de0543ab2..f8351eeec3 100644 --- a/ghost/admin/app/components/gh-member-avatar.js +++ b/ghost/admin/app/components/gh-member-avatar.js @@ -21,7 +21,7 @@ export default Component.extend({ }), backgroundStyle: computed('member.{name,email}', function () { - let name = this.member.name || this.member.email; + let name = this.member.name || this.member.email || 'NM'; if (name) { let color = stringToHslColor(name, 55, 55); return htmlSafe(`background-color: ${color}`); @@ -37,6 +37,8 @@ export default Component.extend({ let intials = names.length > 1 ? [names[0][0], names[names.length - 1][0]] : [names[0][0]]; return intials.join('').toUpperCase(); } - return ''; + + // New Member initials + return 'NM'; }) }); diff --git a/ghost/admin/app/components/gh-member-settings-form.js b/ghost/admin/app/components/gh-member-settings-form.js index 6b27fcc71b..ff63c652fd 100644 --- a/ghost/admin/app/components/gh-member-settings-form.js +++ b/ghost/admin/app/components/gh-member-settings-form.js @@ -2,6 +2,7 @@ import Component from '@ember/component'; import boundOneWay from 'ghost-admin/utils/bound-one-way'; import moment from 'moment'; import {computed} from '@ember/object'; +import {reads} from '@ember/object/computed'; import {inject as service} from '@ember/service'; export default Component.extend({ @@ -15,9 +16,12 @@ export default Component.extend({ setProperty: () => {}, showDeleteTagModal: () => {}, + canEditEmail: reads('member.isNew'), + scratchName: boundOneWay('member.name'), scratchEmail: boundOneWay('member.email'), scratchNote: boundOneWay('member.note'), + subscriptions: computed('member.stripe', function () { let subscriptions = this.member.get('stripe'); if (subscriptions && subscriptions.length > 0) { diff --git a/ghost/admin/app/components/gh-members-list-item.js b/ghost/admin/app/components/gh-members-list-item.js index e97775766b..619ed04ab0 100644 --- a/ghost/admin/app/components/gh-members-list-item.js +++ b/ghost/admin/app/components/gh-members-list-item.js @@ -1,6 +1,6 @@ import Component from '@ember/component'; import moment from 'moment'; -import {alias} from '@ember/object/computed'; +import {alias, or} from '@ember/object/computed'; import {computed} from '@ember/object'; import {inject as service} from '@ember/service'; @@ -17,10 +17,13 @@ export default Component.extend({ id: alias('member.id'), email: alias('member.email'), name: alias('member.name'), - memberSince: computed('member.createdAt', function () { - return moment(this.member.createdAt).from(moment()); + + displayName: or('member.name', 'member.email'), + + memberSince: computed('member.createdAtUTC', function () { + return moment(this.member.createdAtUTC).from(moment()); }), - createdDate: computed('member.createdAt', function () { - return moment(this.member.createdAt).format('MMM DD, YYYY'); + createdDate: computed('member.createdAtUTC', function () { + return moment(this.member.createdAtUTC).format('MMM DD, YYYY'); }) }); diff --git a/ghost/admin/app/controllers/member.js b/ghost/admin/app/controllers/member.js index 8be18affe6..9a7d0da2d4 100644 --- a/ghost/admin/app/controllers/member.js +++ b/ghost/admin/app/controllers/member.js @@ -15,9 +15,9 @@ export default Controller.extend({ notifications: service(), member: alias('model'), - subscribedAt: computed('member.createdAt', function () { - let memberSince = moment(this.member.createdAt).from(moment()); - let createdDate = moment(this.member.createdAt).format('MMM DD, YYYY'); + subscribedAt: computed('member.createdAtUTC', function () { + let memberSince = moment(this.member.createdAtUTC).from(moment()); + let createdDate = moment(this.member.createdAtUTC).format('MMM DD, YYYY'); return `${createdDate} (${memberSince})`; }), @@ -29,7 +29,7 @@ export default Controller.extend({ this.toggleProperty('showDeleteMemberModal'); }, finaliseDeletion() { - // decrememnt the total member count manually so there's no flash + // decrement the total member count manually so there's no flash // when transitioning back to the members list if (this.members.memberCount) { this.members.decrementProperty('memberCount'); diff --git a/ghost/admin/app/controllers/member/new.js b/ghost/admin/app/controllers/member/new.js new file mode 100644 index 0000000000..6eb1e66719 --- /dev/null +++ b/ghost/admin/app/controllers/member/new.js @@ -0,0 +1,104 @@ +import Controller from '@ember/controller'; +import moment from 'moment'; +import {alias} from '@ember/object/computed'; +import {computed} from '@ember/object'; +import {inject as controller} from '@ember/controller'; +import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; + +export default Controller.extend({ + members: controller(), + store: service(), + + router: service(), + + notifications: service(), + + member: alias('model'), + + displayName: computed('member.{name,email}', function () { + return this.member.name || this.member.email || 'New member'; + }), + subscribedAt: computed('member.createdAtUTC', function () { + let memberSince = moment(this.member.createdAtUTC).from(moment()); + let createdDate = moment(this.member.createdAtUTC).format('MMM DD, YYYY'); + return `${createdDate} (${memberSince})`; + }), + + actions: { + setProperty(propKey, value) { + this._saveMemberProperty(propKey, value); + }, + + toggleDeleteMemberModal() { + this.toggleProperty('showDeleteMemberModal'); + }, + + finaliseDeletion() { + // decrement the total member count manually so there's no flash + // when transitioning back to the members list + if (this.members.memberCount) { + this.members.decrementProperty('memberCount'); + } + this.router.transitionTo('members'); + }, + + toggleUnsavedChangesModal(transition) { + let leaveTransition = this.leaveScreenTransition; + + if (!transition && this.showUnsavedChangesModal) { + this.set('leaveScreenTransition', null); + this.set('showUnsavedChangesModal', false); + return; + } + + if (!leaveTransition || transition.targetName === leaveTransition.targetName) { + this.set('leaveScreenTransition', transition); + + // if a save is running, wait for it to finish then transition + if (this.save.isRunning) { + return this.save.last.then(() => { + transition.retry(); + }); + } + + // we genuinely have unsaved data, show the modal + this.set('showUnsavedChangesModal', true); + } + }, + + leaveScreen() { + let transition = this.leaveScreenTransition; + + if (!transition) { + this.notifications.showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'}); + return; + } + + // roll back changes on model props + this.member.rollbackAttributes(); + + return transition.retry(); + }, + + save() { + return this.save.perform(); + } + }, + + save: task(function* () { + let member = this.member; + try { + return yield member.save(); + } catch (error) { + if (error) { + this.notifications.showAPIError(error, {key: 'member.save'}); + } + } + }).drop(), + + _saveMemberProperty(propKey, newValue) { + let member = this.member; + member.set(propKey, newValue); + } +}); diff --git a/ghost/admin/app/controllers/members.js b/ghost/admin/app/controllers/members.js index c43ab6838d..0bb45dc04f 100644 --- a/ghost/admin/app/controllers/members.js +++ b/ghost/admin/app/controllers/members.js @@ -30,7 +30,7 @@ export default Controller.extend({ return (name && name.toLowerCase().indexOf(searchText) >= 0) || (email && email.toLowerCase().indexOf(searchText) >= 0); }).sort((a, b) => { - return b.get('createdAt').valueOf() - a.get('createdAt').valueOf(); + return b.get('createdAtUTC').valueOf() - a.get('createdAtUTC').valueOf(); }); return filtered; @@ -51,7 +51,7 @@ export default Controller.extend({ iframe.setAttribute('src', downloadURL); } }, - + fetchMembers: task(function* () { let newFetchDate = new Date(); diff --git a/ghost/admin/app/mixins/validation-engine.js b/ghost/admin/app/mixins/validation-engine.js index 449f2e5f7f..e4624c3557 100644 --- a/ghost/admin/app/mixins/validation-engine.js +++ b/ghost/admin/app/mixins/validation-engine.js @@ -1,6 +1,7 @@ import DS from 'ember-data'; import IntegrationValidator from 'ghost-admin/validators/integration'; import InviteUserValidator from 'ghost-admin/validators/invite-user'; +import MemberValidator from 'ghost-admin/validators/member'; import Mixin from '@ember/object/mixin'; import Model from 'ember-data/model'; import NavItemValidator from 'ghost-admin/validators/nav-item'; @@ -43,6 +44,7 @@ export default Mixin.create({ slackIntegration: SlackIntegrationValidator, tag: TagSettingsValidator, user: UserValidator, + member: MemberValidator, integration: IntegrationValidator, webhook: WebhookValidator }, diff --git a/ghost/admin/app/models/member.js b/ghost/admin/app/models/member.js index bb06e3b820..c7994c8252 100644 --- a/ghost/admin/app/models/member.js +++ b/ghost/admin/app/models/member.js @@ -1,11 +1,14 @@ -import DS from 'ember-data'; +import Model from 'ember-data/model'; +import ValidationEngine from 'ghost-admin/mixins/validation-engine'; import attr from 'ember-data/attr'; -export default DS.Model.extend({ +export default Model.extend(ValidationEngine, { + validationType: 'member', + name: attr('string'), email: attr('string'), note: attr('string'), - createdAt: attr('moment-utc'), + createdAtUTC: attr('moment-utc'), stripe: attr('member-subscription'), - subscribed: attr('boolean') + subscribed: attr('boolean', {defaultValue: true}) }); diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js index 0172115632..18db7bb18a 100644 --- a/ghost/admin/app/router.js +++ b/ghost/admin/app/router.js @@ -61,6 +61,8 @@ Router.map(function () { this.route('members', function () { this.route('import'); }); + + this.route('member.new', {path: '/members/new'}); this.route('member', {path: '/members/:member_id'}); this.route('error404', {path: '/*path'}); diff --git a/ghost/admin/app/routes/member/new.js b/ghost/admin/app/routes/member/new.js new file mode 100644 index 0000000000..87709594aa --- /dev/null +++ b/ghost/admin/app/routes/member/new.js @@ -0,0 +1,45 @@ +import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; +import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings'; +import {isEmpty} from '@ember/utils'; +import {inject as service} from '@ember/service'; + +export default AuthenticatedRoute.extend(CurrentUserSettings, { + + router: service(), + + controllerName: 'member.new', + templateName: 'member/new', + + init() { + this._super(...arguments); + this.router.on('routeWillChange', (transition) => { + this.showUnsavedChangesModal(transition); + }); + }, + + model() { + return this.store.createRecord('member'); + }, + + // reset the model so that mobile screens react to an empty selectedMember + deactivate() { + this._super(...arguments); + + let {controller} = this; + controller.model.rollbackAttributes(); + controller.set('model', null); + }, + + showUnsavedChangesModal(transition) { + if (transition.from && transition.from.name.match(/^members\.new/) && transition.targetName) { + let {controller} = this; + let isUnchanged = isEmpty(Object.keys(controller.member.changedAttributes())); + if (!controller.member.isDeleted && !isUnchanged) { + transition.abort(); + controller.send('toggleUnsavedChangesModal', transition); + return; + } + } + } + +}); diff --git a/ghost/admin/app/serializers/member.js b/ghost/admin/app/serializers/member.js index 76eff17cb9..0656523960 100644 --- a/ghost/admin/app/serializers/member.js +++ b/ghost/admin/app/serializers/member.js @@ -2,6 +2,10 @@ import ApplicationSerializer from 'ghost-admin/serializers/application'; export default ApplicationSerializer.extend({ + attrs: { + createdAtUTC: {key: 'created_at'} + }, + serialize(/*snapshot, options*/) { let json = this._super(...arguments); diff --git a/ghost/admin/app/styles/layouts/members.css b/ghost/admin/app/styles/layouts/members.css index ea73e800ee..60f49b761d 100644 --- a/ghost/admin/app/styles/layouts/members.css +++ b/ghost/admin/app/styles/layouts/members.css @@ -48,6 +48,12 @@ p.gh-members-list-email { pointer-events: none; } +.members-header .gh-members-header-search { + margin-right: 12px; + border-right: 1px solid var(--lightgrey-d2); + padding-right: 12px; +} + @media (max-width: 1000px) { .members-list .gh-list-header, .gh-list-hidecell-m { display: table-cell; @@ -189,10 +195,14 @@ textarea.gh-member-details-textarea { max-width: 100%; } +.gh-new-member-avatar { + background: var(--midlightgrey-l1); +} + /* Import modal /* ---------------------------------------------------------- */ .gh-members-import-results { margin: 0; width: auto; -} \ No newline at end of file +} diff --git a/ghost/admin/app/templates/components/gh-member-settings-form.hbs b/ghost/admin/app/templates/components/gh-member-settings-form.hbs index 2eabf362ac..8e8e75192e 100644 --- a/ghost/admin/app/templates/components/gh-member-settings-form.hbs +++ b/ghost/admin/app/templates/components/gh-member-settings-form.hbs @@ -1,46 +1,55 @@ -

Basic

-
+

Basic info

+
{{#gh-form-group errors=member.errors hasValidated=member.hasValidated property="name"}} {{gh-text-input - id="member-name" - name="name" - value=(readonly scratchName) - tabindex="1" - input=(action (mut scratchName) value="target.value") - focus-out=(action 'setProperty' 'name' scratchName)}} + id="member-name" + name="name" + value=(readonly scratchName) + tabindex="1" + input=(action (mut scratchName) value="target.value") + focus-out=(action 'setProperty' 'name' scratchName)}} {{gh-error-message errors=member.errors property="name"}} {{/gh-form-group}} - + {{#gh-form-group errors=member.errors hasValidated=member.hasValidated property="email"}} - {{gh-text-input - disabled=true + {{#if canEditEmail}} + {{gh-text-input value=(readonly scratchEmail) id="member-email" name="email" tabindex="2" + autocapitalize="off" + autocorrect="off" + autocomplete="off" focus-out=(action 'setProperty' 'email' scratchEmail) input=(action (mut scratchEmail) value="target.value")}} + {{gh-error-message errors=member.errors property="email"}} + {{else}} + {{gh-text-input + name="email-disabled" + disabled=true + value=(readonly scratchEmail)}} + {{/if}} {{/gh-form-group}}
{{#gh-form-group errors=member.errors hasValidated=member.hasValidated property="note"}} - - {{gh-textarea - id="member-note" - name="note" - class="gh-member-details-textarea" - tabindex="3" - value=(readonly scratchNote) - input=(action (mut scratchNote) value="target.value") - focus-out=(action 'setProperty' 'note' scratchNote) - }} - {{gh-error-message errors=member.errors property="note"}} -

Not visible to member. Maximum: 500 characters. You’ve used {{gh-count-down-characters scratchNote 500}} -

+ + {{gh-textarea + id="member-note" + name="note" + class="gh-member-details-textarea" + tabindex="3" + value=(readonly scratchNote) + input=(action (mut scratchNote) value="target.value") + focus-out=(action 'setProperty' 'note' scratchNote) + }} + {{gh-error-message errors=member.errors property="note"}} +

Not visible to member. Maximum: 500 characters. You’ve used {{gh-count-down-characters scratchNote 500}}

{{/gh-form-group}}
diff --git a/ghost/admin/app/templates/components/gh-members-list-item.hbs b/ghost/admin/app/templates/components/gh-members-list-item.hbs index 5136bb8fc6..828d6d6344 100644 --- a/ghost/admin/app/templates/components/gh-members-list-item.hbs +++ b/ghost/admin/app/templates/components/gh-members-list-item.hbs @@ -2,13 +2,7 @@
-

- {{#if this.name}} - {{this.name}} - {{else}} - {{this.email}} - {{/if}} -

+

{{this.displayName}}

{{#if this.name}}

{{this.email}}

{{/if}} diff --git a/ghost/admin/app/templates/member.hbs b/ghost/admin/app/templates/member.hbs index 196c4b02fb..8c6abbbfd0 100644 --- a/ghost/admin/app/templates/member.hbs +++ b/ghost/admin/app/templates/member.hbs @@ -20,8 +20,8 @@

{{or member.name member.email}}

-

- {{#if member.name}} +

+ {{#if (and member.name member.email)}} {{member.email}} – {{/if}} Created on {{this.subscribedAt}} diff --git a/ghost/admin/app/templates/member/new.hbs b/ghost/admin/app/templates/member/new.hbs new file mode 100644 index 0000000000..9b81ecc14e --- /dev/null +++ b/ghost/admin/app/templates/member/new.hbs @@ -0,0 +1,67 @@ +

+
+ +

+ {{#link-to "members" data-test-link="members-back"}}Members{{/link-to}} + {{svg-jar "arrow-right"}}{{displayName}} +

+
+ {{gh-task-button type="button" task=save class="gh-btn gh-btn-blue gh-btn-icon" data-test-button="save"}} +
+
+
+ {{#if (or member.name member.email)}} + + {{else}} +
+ N +
+ {{/if}} +
+

+ {{#if (or member.name member.email)}} + {{or member.name member.email}} + {{else}} + New member + {{/if}} +

+

+ {{#if (and member.name member.email)}} + {{member.email}} + {{/if}} + {{#unless member.isNew}} + {{if (and member.name member.email) "–"}} + Created on {{this.subscribedAt}} + {{/unless}} +

+
+
+ {{gh-member-settings-form member=member + setProperty=(action "setProperty") + isLoading=this.isLoading + showDeleteTagModal=(action "toggleDeleteMemberModal")}} +
+ + +
+ +{{#if showUnsavedChangesModal}} + {{gh-fullscreen-modal "leave-settings" + confirm=(action "leaveScreen") + close=(action "toggleUnsavedChangesModal") + modifier="action wide"}} +{{/if}} + +{{#if showDeleteMemberModal}} + {{gh-fullscreen-modal "delete-member" + model=(hash member=member onSuccess=(action "finaliseDeletion")) + close=(action (toggle "showDeleteMemberModal" this)) + modifier="action wide"}} +{{/if}} diff --git a/ghost/admin/app/templates/members.hbs b/ghost/admin/app/templates/members.hbs index f403dc7c64..661d41d720 100644 --- a/ghost/admin/app/templates/members.hbs +++ b/ghost/admin/app/templates/members.hbs @@ -26,6 +26,7 @@ {{svg-jar "search" class="gh-input-search-icon"}}
+ {{#link-to "member.new" class="gh-btn gh-btn-green" data-test-new-member-button="true"}}New member{{/link-to}}
diff --git a/ghost/admin/app/validators/member.js b/ghost/admin/app/validators/member.js new file mode 100644 index 0000000000..460bca4521 --- /dev/null +++ b/ghost/admin/app/validators/member.js @@ -0,0 +1,43 @@ +import BaseValidator from './base'; +import validator from 'validator'; +import {isBlank} from '@ember/utils'; + +export default BaseValidator.create({ + properties: ['name', 'email', 'note'], + + name(model) { + if (!validator.isLength(model.name || '', 0, 191)) { + model.errors.add('name', 'Name cannot be longer than 191 characters.'); + this.invalidate(); + } + }, + + email(model) { + let email = model.get('email'); + + if (isBlank(email)) { + model.get('errors').add('email', 'Please enter an email.'); + this.invalidate(); + } else if (!validator.isEmail(email)) { + model.get('errors').add('email', 'Invalid Email.'); + this.invalidate(); + } + if (!validator.isLength(model.name || '', 0, 191)) { + model.errors.add('email', 'Email cannot be longer than 191 characters.'); + this.invalidate(); + } + + model.get('hasValidated').addObject('email'); + }, + + note(model) { + let note = model.get('note'); + + if (!validator.isLength(note || '', 0, 500)) { + model.get('errors').add('note', 'Note is too long.'); + this.invalidate(); + } + + model.get('hasValidated').addObject('note'); + } +}); diff --git a/ghost/admin/mirage/config/members.js b/ghost/admin/mirage/config/members.js index e6c5f4f94e..c9c1c4ea36 100644 --- a/ghost/admin/mirage/config/members.js +++ b/ghost/admin/mirage/config/members.js @@ -1,6 +1,12 @@ import {paginatedResponse} from '../utils'; export default function mockMembers(server) { + server.post('/members/', function ({members}) { + let attrs = this.normalizedRequestAttrs(); + + return members.create(Object.assign({}, attrs, {id: 99})); + }); + server.get('/members/', paginatedResponse('members')); server.get('/members/:id/', function ({members}, {params}) { @@ -15,5 +21,7 @@ export default function mockMembers(server) { }); }); + server.put('/members/:id/'); + server.del('/members/:id/'); } diff --git a/ghost/admin/tests/acceptance/members-test.js b/ghost/admin/tests/acceptance/members-test.js index cfe67c6119..9b1603249b 100644 --- a/ghost/admin/tests/acceptance/members-test.js +++ b/ghost/admin/tests/acceptance/members-test.js @@ -1,9 +1,12 @@ +import moment from 'moment'; +import wait from 'ember-test-helpers/wait'; import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; import {beforeEach, describe, it} from 'mocha'; -import {click, currentRouteName, currentURL, find} from '@ember/test-helpers'; +import {click, currentRouteName, currentURL, fillIn, find, findAll} from '@ember/test-helpers'; import {expect} from 'chai'; import {setupApplicationTest} from 'ember-mocha'; import {setupMirage} from 'ember-cli-mirage/test-support'; +import {timeout} from 'ember-concurrency'; import {visit} from '../helpers/visit'; describe('Acceptance: Members', function () { @@ -53,5 +56,113 @@ describe('Acceptance: Members', function () { expect(currentRouteName()).to.equal('members.index'); expect(find('[data-test-screen-title]')).to.have.text('Members'); }); + + it('it renders, can be navigated, can edit member', async function () { + let member1 = this.server.create('member', {createdAt: moment.utc().subtract(1, 'day').valueOf()}); + this.server.create('member', {createdAt: moment.utc().subtract(2, 'day').valueOf()}); + + await visit('/members'); + + // second wait is needed for the vertical-collection to settle + await wait(); + + // lands on correct page + expect(currentURL(), 'currentURL').to.equal('/members'); + + // it has correct page title + expect(document.title, 'page title').to.equal('Members - Test Blog'); + + // it lists all members + expect(findAll('.members-list .gh-members-list-item').length, 'members list count') + .to.equal(2); + + let member = find('.members-list .gh-members-list-item'); + expect(member.querySelector('.gh-members-list-name').textContent, 'member list item title') + .to.equal(member1.name); + + await visit(`/members/${member1.id}`); + + // // second wait is needed for the member details to settle + await wait(); + + // it shows selected member form + expect(find('.gh-member-basic-settings-form input[name="name"]').value, 'loads correct member into form') + .to.equal(member1.name); + + expect(find('.gh-member-basic-settings-form input[name="email-disabled"]').disabled, 'makes sure email is disabled') + .to.equal(true); + + // trigger save + await fillIn('.gh-member-basic-settings-form input[name="name"]', 'New Name'); + await blur('.gh-member-basic-settings-form input[name="name"]'); + + await click('[data-test-button="save"]'); + + // // extra timeout needed for Travis - sometimes it doesn't update + // // quick enough and an extra wait() call doesn't help + await timeout(100); + + await click('[data-test-link="members-back"]'); + + await wait(); + + // lands on correct page + expect(currentURL(), 'currentURL').to.equal('/members'); + }); + + it('can create a new member', async function () { + this.server.create('member', {createdAt: moment.utc().subtract(1, 'day').valueOf()}); + + await visit('/members'); + + // second wait is needed for the vertical-collection to settle + await wait(); + + // lands on correct page + expect(currentURL(), 'currentURL').to.equal('/members'); + + // it has correct page title + expect(document.title, 'page title').to.equal('Members - Test Blog'); + + // it lists all members + expect(findAll('.members-list .gh-members-list-item').length, 'members list count') + .to.equal(1); + + // start new member + await click('[data-test-new-member-button="true"]'); + + // it navigates to the new member route + expect(currentURL(), 'new member URL').to.equal('/members/new'); + // it displays the new member form + expect(find('.member-basic-info-form .gh-canvas-header h2').textContent, 'settings pane title') + .to.contain('New member'); + + // // all fields start blank + findAll('.gh-member-basic-settings-form input, .gh-member-basic-settings-form textarea').forEach(function (elem) { + expect(elem.value, `input field for ${elem.getAttribute('name')}`) + .to.be.empty; + }); + + expect(find('.gh-member-basic-settings-form input[name="email"]').disabled, 'makes sure email is disabled') + .to.equal(false); + + // save new member + await fillIn('.gh-member-basic-settings-form input[name="name"]', 'New Name'); + await blur('.gh-member-basic-settings-form input[name="name"]'); + + await fillIn('.gh-member-basic-settings-form input[name="email"]', 'example@domain.com'); + await blur('.gh-member-basic-settings-form input[name="email"]'); + + await click('[data-test-button="save"]'); + + // extra timeout needed for FF on Linux - sometimes it doesn't update + // quick enough, especially on Travis, and an extra wait() call + // doesn't help + await timeout(200); + + // disables email field after member has been created + expect(find('.gh-member-basic-settings-form input[name="email-disabled"]').disabled, 'makes sure email is disabled') + .to.equal(true); + }); }); }); diff --git a/ghost/admin/tests/unit/models/member-test.js b/ghost/admin/tests/unit/models/member-test.js index 4d697af0bd..490ac9a585 100644 --- a/ghost/admin/tests/unit/models/member-test.js +++ b/ghost/admin/tests/unit/models/member-test.js @@ -5,10 +5,15 @@ import {setupTest} from 'ember-mocha'; describe('Unit: Model: member', function () { setupTest(); - // Replace this with your real tests. - it('exists', function () { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('member', {}); - expect(model).to.be.ok; + let store; + + beforeEach(function () { + store = this.owner.lookup('service:store'); + }); + + it('has a validation type of "member"', function () { + let model = store.createRecord('member'); + + expect(model.get('validationType')).to.equal('member'); }); });