diff --git a/ghost/admin/app/components/gh-tag-settings-form.hbs b/ghost/admin/app/components/gh-tag-settings-form.hbs index 3ad4ae2059..367e926611 100644 --- a/ghost/admin/app/components/gh-tag-settings-form.hbs +++ b/ghost/admin/app/components/gh-tag-settings-form.hbs @@ -12,6 +12,7 @@ @value={{this.scratchTag.name}} @tabindex="1" @focus-out={{action "setProperty" "name" this.scratchTag.name}} + data-test-input="tag-name" /> <span class="error"> <GhErrorMessage @errors={{this.tag.errors}} @property="name" /> @@ -63,6 +64,7 @@ @name="slug" @tabindex="2" @focus-out={{action "setProperty" "slug" this.scratchTag.slug}} + data-test-input="tag-slug" /> <GhUrlPreview @prefix="tag" @slug={{this.scratchTag.slug}} @tagName="p" @classNames="description" /> <GhErrorMessage @errors={{this.activeTag.errors}} @property="slug" /> diff --git a/ghost/admin/app/components/gh-tags-list-item.hbs b/ghost/admin/app/components/gh-tags-list-item.hbs index c858ac9540..5baddfe10a 100644 --- a/ghost/admin/app/components/gh-tags-list-item.hbs +++ b/ghost/admin/app/components/gh-tags-list-item.hbs @@ -1,17 +1,17 @@ <li class="gh-list-row gh-tags-list-item" ...attributes> <LinkTo @route="tag" @model={{@tag}} class="gh-list-data gh-tag-list-title gh-list-cellwidth-70" title="Edit tag"> - <h3 class="gh-tag-list-name"> + <h3 class="gh-tag-list-name" data-test-tag-name> {{@tag.name}} </h3> {{#if @tag.description}} - <p class="ma0 pa0 f8 midgrey gh-tag-list-description"> + <p class="ma0 pa0 f8 midgrey gh-tag-list-description" data-test-tag-description> {{@tag.description}} </p> {{/if}} </LinkTo> - <LinkTo @route="tag" @model={{@tag}} class="gh-list-data middarkgrey f8 gh-tag-list-slug gh-list-cellwidth-10" title="Edit tag"> - <span title="{{@slug}}">{{@slug}}</span> + <LinkTo @route="tag" @model={{@tag}} class="gh-list-data middarkgrey f8 gh-tag-list-slug gh-list-cellwidth-10" title="Edit tag" data-test-tag-slug> + <span title="{{@tag.slug}}">{{@tag.slug}}</span> </LinkTo> {{#if @tag.count.posts}} diff --git a/ghost/admin/app/components/modal-delete-tag.hbs b/ghost/admin/app/components/modal-delete-tag.hbs index 968c1b5f59..808d8972f6 100644 --- a/ghost/admin/app/components/modal-delete-tag.hbs +++ b/ghost/admin/app/components/modal-delete-tag.hbs @@ -4,13 +4,13 @@ <a class="close" href="" role="button" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a> <div class="modal-body"> - {{#if this.tag.post_count}} - <span class="red">This tag is attached to {{this.tag.count.posts}} {{this.postInflection}}.</span> + {{#if this.tag.count.posts}} + <span class="red">This tag is attached to <span data-test-text="posts-count">{{this.tag.count.posts}} {{this.postInflection}}</span>.</span> {{/if}} You're about to delete "<strong>{{this.tag.name}}</strong>". This is permanent! We warned you, k? </div> <div class="modal-footer"> - <button class="gh-btn" type="button" {{action "closeModal"}}><span>Cancel</span></button> - <GhTaskButton @buttonText="Delete" @successText="Deleted" @task={{this.deleteTag}} @class="gh-btn gh-btn-red gh-btn-icon" /> + <button class="gh-btn" type="button" {{action "closeModal"}} data-test-button="cancel"><span>Cancel</span></button> + <GhTaskButton @buttonText="Delete" @successText="Deleted" @task={{this.deleteTag}} @class="gh-btn gh-btn-red gh-btn-icon" data-test-button="confirm" /> </div> diff --git a/ghost/admin/app/components/modal-delete-tag.js b/ghost/admin/app/components/modal-delete-tag.js index e1d4a459a0..9118fc616d 100644 --- a/ghost/admin/app/components/modal-delete-tag.js +++ b/ghost/admin/app/components/modal-delete-tag.js @@ -4,6 +4,9 @@ import {computed} from '@ember/object'; import {task} from 'ember-concurrency'; export default ModalComponent.extend({ + attributeBindings: ['dataTestModal:data-test-modal'], + dataTestModal: 'confirm-delete-tag', + // Allowed actions confirm: () => {}, diff --git a/ghost/admin/app/controllers/tag.js b/ghost/admin/app/controllers/tag.js index 56f73f120a..c75825c12b 100644 --- a/ghost/admin/app/controllers/tag.js +++ b/ghost/admin/app/controllers/tag.js @@ -45,7 +45,8 @@ export default class TagController extends Controller { deleteTag() { return this.tag.destroyRecord().then(() => { this.set('showDeleteTagModal', false); - return this.transitionToRoute('tags'); + this.router.transitionTo('tags'); + return true; }, (error) => { return this.notifications.showAPIError(error, {key: 'tag.delete'}); }); diff --git a/ghost/admin/app/templates/tag.hbs b/ghost/admin/app/templates/tag.hbs index 0e6538a824..04cc2e25d2 100644 --- a/ghost/admin/app/templates/tag.hbs +++ b/ghost/admin/app/templates/tag.hbs @@ -19,7 +19,7 @@ {{#unless this.tag.isNew}} <div> - <button type="button" class="gh-btn gh-btn-red gh-btn-icon" {{on "click" (action "openDeleteTagModal")}}> + <button type="button" class="gh-btn gh-btn-red gh-btn-icon" {{on "click" (action "openDeleteTagModal")}} data-test-button="delete-tag"> <span>Delete tag</span> </button> </div> diff --git a/ghost/admin/app/templates/tags.hbs b/ghost/admin/app/templates/tags.hbs index ebf4d2e1ed..f144dc3bf7 100644 --- a/ghost/admin/app/templates/tags.hbs +++ b/ghost/admin/app/templates/tags.hbs @@ -3,8 +3,8 @@ <h2 class="gh-canvas-title" data-test-screen-title>Tags</h2> <section class="view-actions"> <div class="gh-contentfilter gh-btn-group"> - <button class="gh-btn {{if (eq this.type "public") "gh-btn-group-selected"}}" type="button" {{action "changeType" "public"}}><span>Public tags</span></button> - <button class="gh-btn {{if (eq this.type "internal") "gh-btn-group-selected"}}" type="button" {{action "changeType" "internal"}}><span>Internal tags</span></button> + <button class="gh-btn {{if (eq this.type "public") "gh-btn-group-selected"}}" type="button" {{action "changeType" "public"}} data-test-tags-nav="public" data-test-active={{eq this.type "public"}}><span>Public tags</span></button> + <button class="gh-btn {{if (eq this.type "internal") "gh-btn-group-selected"}}" type="button" {{action "changeType" "internal"}} data-test-tags-nav="internal" data-test-active={{eq this.type "internal"}}><span>Internal tags</span></button> </div> <LinkTo @route="tag.new" class="gh-btn gh-btn-primary"><span>New tag</span></LinkTo> </section> @@ -20,7 +20,7 @@ <div class="gh-list-header gh-list-cellwidth-10"></div> </li> <VerticalCollection @items={{this.sortedTags}} @key="id" @containerSelector=".gh-main" @estimateHeight={{60}} @bufferSize={{20}} as |tag|> - <GhTagsListItem @tag={{tag}} data-test-tag-id={{tag.id}} /> + <GhTagsListItem @tag={{tag}} data-test-tag={{tag.id}} /> </VerticalCollection> {{else}} <li class="no-posts-box"> diff --git a/ghost/admin/tests/acceptance/settings/tags-test.js b/ghost/admin/tests/acceptance/settings/tags-test.js index a2d0f176d7..6c1a86cc1e 100644 --- a/ghost/admin/tests/acceptance/settings/tags-test.js +++ b/ghost/admin/tests/acceptance/settings/tags-test.js @@ -10,34 +10,7 @@ import {setupMirage} from 'ember-cli-mirage/test-support'; 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: -// https://github.com/madrobby/keymaster/blob/master/test/keymaster.html#L31 -const modifierMap = { - 16: 'shiftKey', - 18: 'altKey', - 17: 'ctrlKey', - 91: 'metaKey' -}; -let keydown = function (code, modifiers, el) { - let event = document.createEvent('Event'); - event.initEvent('keydown', true, true); - event.keyCode = code; - if (modifiers && modifiers.length > 0) { - for (let i in modifiers) { - event[modifierMap[modifiers[i]]] = true; - } - } - (el || document).dispatchEvent(event); -}; -let keyup = function (code, el) { - let event = document.createEvent('Event'); - event.initEvent('keyup', true, true); - event.keyCode = code; - (el || document).dispatchEvent(event); -}; - -describe.skip('Acceptance: Tags', function () { +describe('Acceptance: Tags', function () { let hooks = setupApplicationTest(); setupMirage(hooks); @@ -88,17 +61,18 @@ describe.skip('Acceptance: Tags', function () { windowProxy.replaceState = originalReplaceState; }); - it('it renders, can be navigated, can edit, create & delete tags', async function () { - let tag1 = this.server.create('tag'); - let tag2 = this.server.create('tag'); + it('lists public and internal tags separately', async function () { + 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'}); + this.server.create('tag', {name: '#one', slug: 'hash-one', visibility: 'internal'}); + this.server.create('tag', {name: '#two', slug: 'hash-two', visibility: 'internal'}); - await visit('/tags'); + await visit('tags'); - // it redirects to first tag - // expect(currentURL(), 'currentURL').to.equal(`/tags/${tag1.slug}`); - - // it doesn't redirect to first tag - expect(currentURL(), 'currentURL').to.equal('/tags'); + // it loads tags list + expect(currentURL(), 'currentURL').to.equal('tags'); // it has correct page title expect(document.title, 'page title').to.equal('Tags - Test Blog'); @@ -107,197 +81,84 @@ describe.skip('Acceptance: Tags', function () { expect(find('[data-test-nav="tags"]'), 'highlights nav menu item') .to.have.class('active'); - // it lists all tags - expect(findAll('.tags-list .gh-tags-list-item').length, 'tag list count') - .to.equal(2); - let tag = find('.tags-list .gh-tags-list-item'); - expect(tag.querySelector('.gh-tag-list-name').textContent, 'tag list item title') - .to.equal(tag1.name); + // it defaults to public tags + expect(find('[data-test-tags-nav="public"]')).to.have.attr('data-test-active'); + expect(find('[data-test-tags-nav="internal"]')).to.not.have.attr('data-test-active'); - // it highlights selected tag - // expect(find(`a[href="/ghost/tags/${tag1.slug}"]`), 'highlights selected tag') - // .to.have.class('active'); + // it lists all public tags + expect(findAll('[data-test-tag]'), 'public tag list count') + .to.have.length(4); - await visit(`/tags/${tag1.slug}`); + // tags are in correct order + let tags = findAll('[data-test-tag]'); - // second wait is needed for the tag details to settle + expect(tags[0].querySelector('[data-test-tag-name]')).to.have.trimmed.text('A - First'); + expect(tags[1].querySelector('[data-test-tag-name]')).to.have.trimmed.text('!A - Second'); + expect(tags[2].querySelector('[data-test-tag-name]')).to.have.trimmed.text('B - Third'); + expect(tags[3].querySelector('[data-test-tag-name]')).to.have.trimmed.text('Z - Last'); - // it shows selected tag form - // expect(find('.tag-settings-pane h4').textContent, 'settings pane title') - // .to.equal('Tag settings'); - expect(find('.gh-tag-basic-settings-form input[name="name"]').value, 'loads correct tag into form') - .to.equal(tag1.name); + // can switch to internal tags + await click('[data-test-tags-nav="internal"]'); - // click the second tag in the list - // 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(`/tags/${tag2.slug}`); - - // it highlights selected tag - // expect(find(`a[href="/ghost/tags/${tag2.slug}"]`), 'highlights selected tag') - // .to.have.class('active'); - - // it shows selected tag form - // expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form') - // .to.equal(tag2.name); - - // simulate up arrow press - run(() => { - keydown(38); - keyup(38); - }); - - await settled(); - - // it navigates to previous tag - expect(currentURL(), 'url after keyboard up arrow').to.equal(`/tags/${tag1.slug}`); - - // it highlights selected tag - // expect(find(`a[href="/ghost/tags/${tag1.slug}"]`), 'selects previous tag') - // .to.have.class('active'); - - // simulate down arrow press - run(() => { - keydown(40); - keyup(40); - }); - - await settled(); - - // it navigates to previous tag - expect(currentURL(), 'url after keyboard down arrow').to.equal(`/tags/${tag2.slug}`); - - // it highlights selected tag - // expect(find(`a[href="/ghost/tags/${tag2.slug}"]`), 'selects next tag') - // .to.have.class('active'); - - // trigger save - await fillIn('.tag-settings-pane input[name="name"]', 'New Name'); - 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 - 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"]').value, 'settings form updates on save') - .to.equal('New Name'); - - // start new tag - await click('.view-actions .gh-btn-green'); - - // it navigates to the new tag route - expect(currentURL(), 'new tag URL').to.equal('/tags/new'); - - // it displays the new tag form - expect(find('.tag-settings-pane h4').textContent, 'settings pane title') - .to.equal('New tag'); - - // all fields start blank - 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 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 - // doesn't help - await timeout(100); - - // it redirects to the new tag's URL - expect(currentURL(), 'URL after tag creation').to.equal('/tags/new-tag'); - - // it adds the tag to the list and selects - 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(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/tags/new-tag"]'), 'highlights new tag') - .to.have.class('active'); - - // delete tag - await click('.settings-menu-delete-button'); - await click('.fullscreen-modal .gh-btn-red'); - - // it redirects to the first tag - expect(currentURL(), 'URL after tag deletion').to.equal(`/tags/${tag1.slug}`); - - // it removes the tag from the list - expect(findAll('.settings-tags .settings-tag').length, 'tag list count after deletion') - .to.equal(2); + expect(findAll('[data-test-tag]'), 'internal tag list count').to.have.length(2); }); - // TODO: Unskip and fix - // 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 () { - this.server.createList('tag', 2); + it('can edit tags', async function () { + const tag = this.server.create('tag', {name: 'To be edited', slug: 'to-be-edited'}); - await visit('/tags/tag-1'); + await visit('tags'); + await click(`[data-test-tag="${tag.id}"] [data-test-tag-name]`); - expect(currentURL(), 'URL after direct load').to.equal('/tags/tag-1'); + expect(currentURL()).to.equal('/tags/to-be-edited'); - // it loads all other tags - expect(findAll('.settings-tags .settings-tag').length, 'tag list count after direct load') - .to.equal(2); + expect(find('[data-test-input="tag-name"]')).to.have.value('To be edited'); + expect(find('[data-test-input="tag-slug"]')).to.have.value('to-be-edited'); - // selects tag in list - expect(find('a[href="/ghost/tags/tag-1"]').classList.contains('active'), 'highlights requested tag') - .to.be.true; + await fillIn('[data-test-input="tag-name"]', 'New tag name'); + await fillIn('[data-test-input="tag-slug"]', 'new-tag-slug'); + await click('[data-test-button="save"]'); - // shows requested tag in settings pane - expect(find('.tag-settings-pane input[name="name"]').value, 'loads correct tag into form') - .to.equal('Tag 1'); + const savedTag = this.server.db.tags.find(tag.id); + expect(savedTag.name, 'saved tag name').to.equal('New tag name'); + expect(savedTag.slug, 'saved tag slug').to.equal('new-tag-slug'); + + await click('[data-test-link="tags-back"]'); + + const tagListItem = find('[data-test-tag]'); + expect(tagListItem.querySelector('[data-test-tag-name]')).to.have.trimmed.text('New tag name'); + expect(tagListItem.querySelector('[data-test-tag-slug]')).to.have.trimmed.text('new-tag-slug'); }); - it('shows the internal tag label', async function () { - this.server.create('tag', {name: '#internal-tag', slug: 'hash-internal-tag', visibility: 'internal'}); + it('can delete tags', async function () { + const tag = this.server.create('tag', {name: 'To be edited', slug: 'to-be-edited'}); + this.server.create('post', {tags: [tag]}); - await visit('tags/'); + await visit('tags'); + await click(`[data-test-tag="${tag.id}"] [data-test-tag-name]`); - expect(currentURL()).to.equal('/tags/hash-internal-tag'); + await click('[data-test-button="delete-tag"]'); - expect(findAll('.settings-tags .settings-tag').length, 'tag list count') - .to.equal(1); + const tagModal = '[data-test-modal="confirm-delete-tag"]'; - let tag = find('.settings-tags .settings-tag'); + expect(find(tagModal)).to.exist; + expect(find(`${tagModal} [data-test-text="posts-count"]`)) + .to.have.trimmed.text('1 post'); - expect(tag.querySelectorAll('.label.label-blue').length, 'internal tag label') - .to.equal(1); + await click(`${tagModal} [data-test-button="confirm"]`); - expect(tag.querySelector('.label.label-blue').textContent.trim(), 'internal tag label text') - .to.equal('internal'); + expect(find(tagModal)).to.not.exist; + expect(currentURL()).to.equal('/tags'); + expect(findAll('[data-test-tag]')).to.have.length(0); }); - it('updates the URL when slug changes', async function () { - this.server.createList('tag', 2); + it('can load tag via slug in url', async function () { + const tag = this.server.create('tag', {name: 'To be edited', slug: 'to-be-edited'}); - await visit('/tags/tag-1'); + await visit('tags/to-be-edited'); + expect(currentURL()).to.equal('tags/to-be-edited'); - expect(currentURL(), 'URL after direct load').to.equal('/tags/tag-1'); - - // update the slug - await fillIn('.tag-settings-pane input[name="slug"]', 'test'); - 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 - expect(newLocation, 'URL after slug change').to.equal('test'); + expect(find('[data-test-input="tag-name"]')).to.have.value('To be edited'); + expect(find('[data-test-input="tag-slug"]')).to.have.value('to-be-edited'); }); it('redirects to 404 when tag does not exist', async function () { @@ -310,21 +171,5 @@ describe.skip('Acceptance: Tags', function () { expect(currentRouteName()).to.equal('error404'); expect(currentURL()).to.equal('/tags/unknown'); }); - - it('sorts tags correctly', async function () { - 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('tags'); - - 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('#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'); - }); }); });