Added posts bulk action Admin tests (#20610)

ref https://linear.app/tryghost/issue/ENG-1360

Not *all* functionality has been covered by these tests. There's a few
missing pieces from our mirage build and use that likely doesn't need
full coverage within the admin package. Regardless, this view has
dramatically more coverage at this point.
This commit is contained in:
Steve Larson 2024-07-16 12:56:15 -05:00 committed by GitHub
parent bb18e6571e
commit 2e3eb1da71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 393 additions and 75 deletions

View File

@ -93,4 +93,31 @@ export default function mockPosts(server) {
});
server.del('/posts/:id/');
server.del('/posts/', function ({posts}, {queryParams}) {
let ids = extractFilterParam('id', queryParams.filter);
posts.find(ids).destroy();
});
server.put('/posts/bulk/', function ({tags}, {requestBody}) {
const bulk = JSON.parse(requestBody).bulk;
const action = bulk.action;
// const ids = extractFilterParam('id', queryParams.filter);
if (action === 'addTag') {
// create tag so we have an id from the server
const newTags = bulk.meta.tags;
// check applied tags to see if any new ones should be created
newTags.forEach((tag) => {
if (!tag.id) {
tags.create(tag);
}
});
// TODO: update the actual posts in the mock db
// const postsToUpdate = posts.find(ids);
// getting the posts is fine, but within this we CANNOT manipulate them (???) not even iterate with .forEach
}
});
}

View File

@ -1,11 +1,22 @@
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha';
import {blur, click, currentURL, fillIn, find, findAll, settled, visit} from '@ember/test-helpers';
import {blur, click, currentURL, fillIn, find, findAll, triggerEvent, triggerKeyEvent, visit} from '@ember/test-helpers';
import {clickTrigger, selectChoose} from 'ember-power-select/test-support/helpers';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {setupMirage} from 'ember-cli-mirage/test-support';
/**
*
* @param {string} text
* @param {NodeList} buttons
* @returns Node
*/
const findButton = (text, buttons) => {
return Array.from(buttons).find(button => button.innerText.trim() === text);
};
describe('Acceptance: Content', function () {
let hooks = setupApplicationTest();
setupMirage(hooks);
@ -32,8 +43,9 @@ describe('Acceptance: Content', function () {
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', visibility: 'paid'});
draftPost = this.server.create('post', {authors: [admin], status: 'draft', title: 'Draft Post'});
authorPost = this.server.create('post', {authors: [editor], status: 'published', title: 'Editor Published Post'});
authorPost = this.server.create('post', {authors: [editor], status: 'published', title: 'Editor Published Post', visibiity: 'paid'});
// pages shouldn't appear in the list
this.server.create('page', {authors: [admin], status: 'published', title: 'Published Page'});
@ -41,97 +53,376 @@ describe('Acceptance: Content', function () {
return await authenticateSession();
});
it.skip('displays and filters posts', async function () {
await visit('/posts');
// Not checking request here as it won't be the last request made
// Displays all posts + pages
expect(findAll('[data-test-post-id]').length, 'all posts count').to.equal(4);
describe('displays and filter posts', function () {
it('displays posts', async function () {
await visit('/posts');
// show draft posts
await selectChoose('[data-test-type-select]', 'Draft posts');
const posts = findAll('[data-test-post-id]');
// displays all posts by default (all statuses) [no pages]
expect(posts.length, 'all posts count').to.equal(4);
// API request is correct
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"drafts" request status filter').to.have.string('status:draft');
// Displays draft post
expect(findAll('[data-test-post-id]').length, 'drafts count').to.equal(1);
expect(find(`[data-test-post-id="${draftPost.id}"]`), 'draft post').to.exist;
// note: atm the mirage backend doesn't support ordering of the results set
});
// show published posts
await selectChoose('[data-test-type-select]', 'Published posts');
it('can filter by status', async function () {
await visit('/posts');
// API request is correct
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"published" request status filter').to.have.string('status:published');
// Displays three published posts + pages
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;
// show draft posts
await selectChoose('[data-test-type-select]', 'Draft posts');
// show scheduled posts
await selectChoose('[data-test-type-select]', 'Scheduled posts');
// API request is correct
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"drafts" request status filter').to.have.string('status:draft');
// Displays draft post
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] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"published" request status filter').to.have.string('status:published');
// Displays three published posts + pages
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;
// show scheduled posts
await selectChoose('[data-test-type-select]', 'Scheduled posts');
// API request is correct
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"scheduled" request status filter').to.have.string('status:scheduled');
// Displays scheduled post
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 all posts
await selectChoose('[data-test-type-select]', 'All posts');
// API request is correct
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"all" request status filter').to.have.string('status:[draft,scheduled,published,sent]');
});
// API request is correct
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"scheduled" request status filter').to.have.string('status:scheduled');
// Displays scheduled post
expect(findAll('[data-test-post-id]').length, 'scheduled count').to.equal(1);
expect(find(`[data-test-post-id="${scheduledPost.id}"]`), 'scheduled post').to.exist;
it('can filter by author', async function () {
await visit('/posts');
// show all posts
await selectChoose('[data-test-type-select]', 'All posts');
// show all posts by editor
await selectChoose('[data-test-author-select]', editor.name);
// API request is correct
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"all" request status filter').to.have.string('status:[draft,scheduled,published]');
// API request is correct
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"editor" request status filter')
.to.have.string('status:[draft,scheduled,published,sent]');
expect(lastRequest.queryParams.filter, '"editor" request filter param')
.to.have.string(`authors:${editor.slug}`);
});
// show all posts by editor
await selectChoose('[data-test-author-select]', editor.name);
it('can filter by visibility', async function () {
await visit('/posts');
// API request is correct
[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 filter param')
.to.have.string(`authors:${editor.slug}`);
await selectChoose('[data-test-visibility-select]', 'Paid members-only');
// Post status is only visible when members is enabled
expect(find('[data-test-visibility-select]'), 'access dropdown before members enabled').to.not.exist;
let featureService = this.owner.lookup('service:feature');
featureService.set('members', true);
await settled();
expect(find('[data-test-visibility-select]'), 'access dropdown after members enabled').to.exist;
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"visibility" request filter param')
.to.have.string('visibility:[paid,tiers]+status:[draft,scheduled,published,sent]');
});
await selectChoose('[data-test-visibility-select]', 'Paid members-only');
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"visibility" request filter param')
.to.have.string('visibility:[paid,tiers]+status:[draft,scheduled,published]');
it('can filter by tag', async function () {
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'});
// Displays editor post
// TODO: implement "filter" param support and fix mirage post->author association
// expect(find('[data-test-post-id]').length, 'editor post count').to.equal(1);
// expect(find(`[data-test-post-id="${authorPost.id}"]`), 'author post').to.exist;
await visit('/posts');
await clickTrigger('[data-test-tag-select]');
// TODO: test tags dropdown
let options = findAll('.ember-power-select-option');
// check that dropdown sorts alphabetically
expect(options[0].textContent.trim()).to.equal('All tags');
expect(options[1].textContent.trim()).to.equal('A - First');
expect(options[2].textContent.trim()).to.equal('B - Second');
expect(options[3].textContent.trim()).to.equal('Z - Last');
// select one
await selectChoose('[data-test-tag-select]', 'B - Second');
// affirm request
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, 'request filter').to.have.string('tag:second');
});
});
// TODO: skipped due to consistently random failures on Travis
// options[0] is undefined
// https://github.com/TryGhost/Ghost/issues/10308
it.skip('sorts tags filter alphabetically', async function () {
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'});
describe('context menu actions', function () {
describe('single post', function () {
// has a duplicate option
it.skip('can duplicate a post', async function () {
await visit('/posts');
await visit('/posts');
await clickTrigger('[data-test-tag-select]');
// get the post
const post = find(`[data-test-post-id="${publishedPost.id}"]`);
expect(post, 'post').to.exist;
let options = findAll('.ember-power-select-option');
await triggerEvent(post, 'contextmenu');
// await this.pauseTest();
expect(options[0].textContent.trim()).to.equal('All tags');
expect(options[1].textContent.trim()).to.equal('A - First');
expect(options[2].textContent.trim()).to.equal('B - Second');
expect(options[3].textContent.trim()).to.equal('Z - Last');
let contextMenu = find('.gh-posts-context-menu'); // this is a <ul> element
let buttons = contextMenu.querySelectorAll('button');
// should have three options for a published post
expect(contextMenu, 'context menu').to.exist;
expect(buttons.length, 'context menu buttons').to.equal(5);
expect(buttons[0].innerText.trim(), 'context menu button 1').to.contain('Unpublish');
expect(buttons[1].innerText.trim(), 'context menu button 2').to.contain('Feature'); // or Unfeature
expect(buttons[2].innerText.trim(), 'context menu button 3').to.contain('Add a tag');
expect(buttons[3].innerText.trim(), 'context menu button 4').to.contain('Duplicate');
expect(buttons[4].innerText.trim(), 'context menu button 5').to.contain('Delete');
// duplicate the post
await click(buttons[3]);
// API request is correct
// POST /ghost/api/admin/posts/{id}/copy/?formats=mobiledoc,lexical
// TODO: probably missing endpoint in mirage...
// let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
// console.log(`lastRequest`, lastRequest);
// expect(lastRequest.url, 'request url').to.match(new RegExp(`/posts/${publishedPost.id}/copy/`));
});
});
describe('multiple posts', function () {
it('can feature and unfeature posts', async function () {
await visit('/posts');
// get all posts
const posts = findAll('[data-test-post-id]');
expect(posts.length, 'all posts count').to.equal(4);
const postThreeContainer = posts[2].parentElement; // draft post
const postFourContainer = posts[3].parentElement; // published post
await click(postThreeContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'});
await click(postFourContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'});
expect(postFourContainer.getAttribute('data-selected'), 'postFour selected').to.exist;
expect(postThreeContainer.getAttribute('data-selected'), 'postThree selected').to.exist;
// NOTE: right clicks don't seem to work in these tests
// contextmenu is the event triggered - https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
await triggerEvent(postFourContainer, 'contextmenu');
let contextMenu = find('.gh-posts-context-menu'); // this is a <ul> element
expect(contextMenu, 'context menu').to.exist;
// feature the post
let buttons = contextMenu.querySelectorAll('button');
let featureButton = findButton('Feature', buttons);
expect(featureButton, 'feature button').to.exist;
await click(featureButton);
// API request is correct - note, we don't mock the actual model updates
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, 'feature request id').to.equal(`id:['3','4']`);
expect(JSON.parse(lastRequest.requestBody).bulk.action, 'feature request action').to.equal('feature');
// ensure ui shows these are now featured
expect(postThreeContainer.querySelector('.gh-featured-post'), 'postFour featured').to.exist;
expect(postFourContainer.querySelector('.gh-featured-post'), 'postFour featured').to.exist;
// unfeature the posts
await triggerEvent(postFourContainer, 'contextmenu');
contextMenu = find('.gh-posts-context-menu'); // this is a <ul> element
expect(contextMenu, 'context menu').to.exist;
// unfeature the posts
buttons = contextMenu.querySelectorAll('button');
featureButton = findButton('Unfeature', buttons);
expect(featureButton, 'unfeature button').to.exist;
await click(featureButton);
// API request is correct - note, we don't mock the actual model updates
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, 'unfeature request id').to.equal(`id:['3','4']`);
expect(JSON.parse(lastRequest.requestBody).bulk.action, 'unfeature request action').to.equal('unfeature');
// ensure ui shows these are now unfeatured
expect(postThreeContainer.querySelector('.gh-featured-post'), 'postFour featured').to.not.exist;
expect(postFourContainer.querySelector('.gh-featured-post'), 'postFour featured').to.not.exist;
});
it('can add a tag to multiple posts', async function () {
await visit('/posts');
// get all posts
const posts = findAll('[data-test-post-id]');
expect(posts.length, 'all posts count').to.equal(4);
const postThreeContainer = posts[2].parentElement; // draft post
const postFourContainer = posts[3].parentElement; // published post
await click(postThreeContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'});
await click(postFourContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'});
expect(postFourContainer.getAttribute('data-selected'), 'postFour selected').to.exist;
expect(postThreeContainer.getAttribute('data-selected'), 'postThree selected').to.exist;
// NOTE: right clicks don't seem to work in these tests
// contextmenu is the event triggered - https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
await triggerEvent(postFourContainer, 'contextmenu');
let contextMenu = find('.gh-posts-context-menu'); // this is a <ul> element
expect(contextMenu, 'context menu').to.exist;
// add a tag to the posts
let buttons = contextMenu.querySelectorAll('button');
let addTagButton = findButton('Add a tag', buttons);
expect(addTagButton, 'add tag button').to.exist;
await click(addTagButton);
const addTagsModal = find('[data-test-modal="add-tags"]');
expect(addTagsModal, 'tag settings modal').to.exist;
const input = addTagsModal.querySelector('input');
expect(input, 'tag input').to.exist;
await fillIn(input, 'test-tag');
await triggerKeyEvent(input, 'keydown', 13);
await click('[data-test-button="confirm"]');
// API request is correct - note, we don't mock the actual model updates
let [lastRequest] = this.server.pretender.handledRequests.slice(-2);
expect(lastRequest.queryParams.filter, 'add tag request id').to.equal(`id:['3','4']`);
expect(JSON.parse(lastRequest.requestBody).bulk.action, 'add tag request action').to.equal('addTag');
});
// NOTE: we do not seem to be loading the settings properly into the membersutil service, such that the members
// service doesn't think members are enabled
it.skip('can change access to multiple posts', async function () {
await visit('/posts');
// get all posts
const posts = findAll('[data-test-post-id]');
expect(posts.length, 'all posts count').to.equal(4);
const postThreeContainer = posts[2].parentElement; // draft post
const postFourContainer = posts[3].parentElement; // published post
await click(postThreeContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'});
await click(postFourContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'});
expect(postFourContainer.getAttribute('data-selected'), 'postFour selected').to.exist;
expect(postThreeContainer.getAttribute('data-selected'), 'postThree selected').to.exist;
// NOTE: right clicks don't seem to work in these tests
// contextmenu is the event triggered - https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
await triggerEvent(postFourContainer, 'contextmenu');
let contextMenu = find('.gh-posts-context-menu'); // this is a <ul> element
expect(contextMenu, 'context menu').to.exist;
// TODO: the change access button is not showing; need to debug the UI to see what field it expects
// change access to the posts
let buttons = contextMenu.querySelectorAll('button');
let changeAccessButton = findButton('Change access', buttons);
expect(changeAccessButton, 'change access button').to.exist;
await click(changeAccessButton);
const changeAccessModal = find('[data-test-modal="edit-posts-access"]');
expect(changeAccessModal, 'change access modal').to.exist;
});
it('can unpublish posts', async function () {
await visit('/posts');
// get all posts
const posts = findAll('[data-test-post-id]');
expect(posts.length, 'all posts count').to.equal(4);
const postThreeContainer = posts[2].parentElement; // draft post
const postFourContainer = posts[3].parentElement; // published post
await click(postThreeContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'});
await click(postFourContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'});
expect(postFourContainer.getAttribute('data-selected'), 'postFour selected').to.exist;
expect(postThreeContainer.getAttribute('data-selected'), 'postThree selected').to.exist;
// NOTE: right clicks don't seem to work in these tests
// contextmenu is the event triggered - https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
await triggerEvent(postFourContainer, 'contextmenu');
let contextMenu = find('.gh-posts-context-menu'); // this is a <ul> element
expect(contextMenu, 'context menu').to.exist;
// unpublish the posts
let buttons = contextMenu.querySelectorAll('button');
let unpublishButton = findButton('Unpublish', buttons);
expect(unpublishButton, 'unpublish button').to.exist;
await click(unpublishButton);
// handle modal
const modal = find('[data-test-modal="unpublish-posts"]');
expect(modal, 'unpublish modal').to.exist;
await click('[data-test-button="confirm"]');
// API request is correct - note, we don't mock the actual model updates
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, 'unpublish request id').to.equal(`id:['3','4']`);
expect(JSON.parse(lastRequest.requestBody).bulk.action, 'unpublish request action').to.equal('unpublish');
// ensure ui shows these are now unpublished
expect(postThreeContainer.querySelector('.gh-content-entry-status').textContent, 'postThree status').to.contain('Draft');
expect(postFourContainer.querySelector('.gh-content-entry-status').textContent, 'postThree status').to.contain('Draft');
});
it('can delete posts', async function () {
await visit('/posts');
// get all posts
const posts = findAll('[data-test-post-id]');
expect(posts.length, 'all posts count').to.equal(4);
const postThreeContainer = posts[2].parentElement; // draft post
const postFourContainer = posts[3].parentElement; // published post
await click(postThreeContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'});
await click(postFourContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'});
expect(postFourContainer.getAttribute('data-selected'), 'postFour selected').to.exist;
expect(postThreeContainer.getAttribute('data-selected'), 'postThree selected').to.exist;
// NOTE: right clicks don't seem to work in these tests
// contextmenu is the event triggered - https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
await triggerEvent(postFourContainer, 'contextmenu');
let contextMenu = find('.gh-posts-context-menu'); // this is a <ul> element
expect(contextMenu, 'context menu').to.exist;
// delete the posts
let buttons = contextMenu.querySelectorAll('button');
let deleteButton = findButton('Delete', buttons);
expect(deleteButton, 'delete button').to.exist;
await click(deleteButton);
// handle modal
const modal = find('[data-test-modal="delete-posts"]');
expect(modal, 'delete modal').to.exist;
await click('[data-test-button="confirm"]');
// API request is correct - note, we don't mock the actual model updates
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, 'delete request id').to.equal(`id:['3','4']`);
expect(lastRequest.method, 'delete request method').to.equal('DELETE');
// ensure ui shows these are now deleted
expect(findAll('[data-test-post-id]').length, 'all posts count').to.equal(2);
});
});
});
it('can add and edit custom views', async function () {
@ -280,4 +571,4 @@ describe('Acceptance: Content', function () {
expect(find('[data-test-no-posts-box]')).to.not.exist;
});
});
});
});