From c61c42ce1dc64891854f5591a1796a2cd736452a Mon Sep 17 00:00:00 2001
From: Steve Larson <9larsons@gmail.com>
Date: Mon, 29 Jul 2024 11:19:28 -0500
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Improved=20performance=20loading=20?=
=?UTF-8?q?posts=20&=20pages=20in=20admin=20(#20646)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
ref 8ea1dfb
ref https://linear.app/tryghost/issue/ONC-111
* undid the reversion for the performance improvements
* built upon new tests for the posts list functionality in admin,
including right click actions
* added tests for pages view in Admin
This was reverted because it broke the Pages list view in Admin, which
is a thin extension of the Posts functionality in admin (route &
controller). That has been fixed and tests added.
This was originally reverted because the changes to improve loading
response times broke right click (bulk) actions in the posts list. This
was not caught because it turned out we had near-zero test coverage of
that part of the codebase. Test coverage has been expanded for the posts
list, and while not comprehensive, is a much better place for us to be
in.
---
ghost/admin/app/components/gh-context-menu.js | 2 +-
.../app/components/posts-list/context-menu.js | 39 +-
.../admin/app/components/posts-list/list.hbs | 41 +-
.../posts-list}/selection-list.js | 98 +-
ghost/admin/app/controllers/pages.js | 2 +-
ghost/admin/app/controllers/posts.js | 10 +-
ghost/admin/app/routes/posts.js | 56 +-
ghost/admin/app/templates/pages.hbs | 23 +-
ghost/admin/app/templates/posts.hbs | 23 +-
ghost/admin/mirage/config/pages.js | 1 -
ghost/admin/mirage/config/posts.js | 25 +-
ghost/admin/tests/acceptance/content-test.js | 1143 +++++++++--------
12 files changed, 834 insertions(+), 629 deletions(-)
rename ghost/admin/app/{utils => components/posts-list}/selection-list.js (67%)
diff --git a/ghost/admin/app/components/gh-context-menu.js b/ghost/admin/app/components/gh-context-menu.js
index 8b3de5a54b..205e488bb1 100644
--- a/ghost/admin/app/components/gh-context-menu.js
+++ b/ghost/admin/app/components/gh-context-menu.js
@@ -1,5 +1,5 @@
import Component from '@glimmer/component';
-import SelectionList from '../utils/selection-list';
+import SelectionList from './posts-list/selection-list';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
diff --git a/ghost/admin/app/components/posts-list/context-menu.js b/ghost/admin/app/components/posts-list/context-menu.js
index f945cfda40..3eabecb4a1 100644
--- a/ghost/admin/app/components/posts-list/context-menu.js
+++ b/ghost/admin/app/components/posts-list/context-menu.js
@@ -216,11 +216,14 @@ export default class PostsContextMenu extends Component {
yield this.performBulkDestroy();
this.notifications.showNotification(this.#getToastMessage('deleted'), {type: 'success'});
- const remainingModels = this.selectionList.infinityModel.content.filter((model) => {
- return !deletedModels.includes(model);
- });
- // Deleteobjects method from infintiymodel is broken for all models except the first page, so we cannot use this
- this.infinity.replace(this.selectionList.infinityModel, remainingModels);
+ for (const key in this.selectionList.infinityModel) {
+ const remainingModels = this.selectionList.infinityModel[key].content.filter((model) => {
+ return !deletedModels.includes(model);
+ });
+ // Deleteobjects method from infintiymodel is broken for all models except the first page, so we cannot use this
+ this.infinity.replace(this.selectionList.infinityModel[key], remainingModels);
+ }
+
this.selectionList.clearSelection({force: true});
return true;
}
@@ -247,9 +250,7 @@ export default class PostsContextMenu extends Component {
}
}
- // Remove posts that no longer match the filter
this.updateFilteredPosts();
-
return true;
}
@@ -282,14 +283,16 @@ export default class PostsContextMenu extends Component {
]
});
- const remainingModels = this.selectionList.infinityModel.content.filter((model) => {
- if (!updatedModels.find(u => u.id === model.id)) {
- return true;
- }
- return filterNql.queryJSON(model.serialize({includeId: true}));
- });
- // Deleteobjects method from infintiymodel is broken for all models except the first page, so we cannot use this
- this.infinity.replace(this.selectionList.infinityModel, remainingModels);
+ for (const key in this.selectionList.infinityModel) {
+ const remainingModels = this.selectionList.infinityModel[key].content.filter((model) => {
+ if (!updatedModels.find(u => u.id === model.id)) {
+ return true;
+ }
+ return filterNql.queryJSON(model.serialize({includeId: true}));
+ });
+ // Deleteobjects method from infintiymodel is broken for all models except the first page, so we cannot use this
+ this.infinity.replace(this.selectionList.infinityModel[key], remainingModels);
+ }
this.selectionList.clearUnavailableItems();
}
@@ -386,8 +389,10 @@ export default class PostsContextMenu extends Component {
const data = result[this.type === 'post' ? 'posts' : 'pages'][0];
const model = this.store.peekRecord(this.type, data.id);
- // Update infinity list
- this.selectionList.infinityModel.content.unshiftObject(model);
+ // Update infinity draft posts content - copied posts are always drafts
+ if (this.selectionList.infinityModel.draftInfinityModel) {
+ this.selectionList.infinityModel.draftInfinityModel.content.unshiftObject(model);
+ }
// Show notification
this.notifications.showNotification(this.#getToastMessage('duplicated'), {type: 'success'});
diff --git a/ghost/admin/app/components/posts-list/list.hbs b/ghost/admin/app/components/posts-list/list.hbs
index 4755c76d62..99e0dcf7d5 100644
--- a/ghost/admin/app/components/posts-list/list.hbs
+++ b/ghost/admin/app/components/posts-list/list.hbs
@@ -1,14 +1,39 @@
- {{#each @model as |post|}}
-
-
-
+ {{!-- always order as scheduled, draft, remainder --}}
+ {{#if (or @model.scheduledInfinityModel (or @model.draftInfinityModel @model.publishedAndSentInfinityModel))}}
+ {{#if @model.scheduledInfinityModel}}
+ {{#each @model.scheduledInfinityModel as |post|}}
+
+
+
+ {{/each}}
+ {{/if}}
+ {{#if (and @model.draftInfinityModel (or (not @model.scheduledInfinityModel) (and @model.scheduledInfinityModel @model.scheduledInfinityModel.reachedInfinity)))}}
+ {{#each @model.draftInfinityModel as |post|}}
+
+
+
+ {{/each}}
+ {{/if}}
+ {{#if (and @model.publishedAndSentInfinityModel (and (or (not @model.scheduledInfinityModel) @model.scheduledInfinityModel.reachedInfinity) (or (not @model.draftInfinityModel) @model.draftInfinityModel.reachedInfinity)))}}
+ {{#each @model.publishedAndSentInfinityModel as |post|}}
+
+
+
+ {{/each}}
+ {{/if}}
{{else}}
{{yield}}
- {{/each}}
+ {{/if}}
{{!-- The currently selected item or items are passed to the context menu --}}
diff --git a/ghost/admin/app/utils/selection-list.js b/ghost/admin/app/components/posts-list/selection-list.js
similarity index 67%
rename from ghost/admin/app/utils/selection-list.js
rename to ghost/admin/app/components/posts-list/selection-list.js
index b409d6da4b..ec45475be0 100644
--- a/ghost/admin/app/utils/selection-list.js
+++ b/ghost/admin/app/components/posts-list/selection-list.js
@@ -18,7 +18,11 @@ export default class SelectionList {
#clearOnNextUnfreeze = false;
constructor(infinityModel) {
- this.infinityModel = infinityModel ?? {content: []};
+ this.infinityModel = infinityModel ?? {
+ draftInfinityModel: {
+ content: []
+ }
+ };
}
freeze() {
@@ -41,7 +45,12 @@ export default class SelectionList {
* Returns an NQL filter for all items, not the selection
*/
get allFilter() {
- return this.infinityModel.extraParams?.filter ?? '';
+ const models = this.infinityModel;
+ // grab filter from the first key in the infinityModel object (they should all be identical)
+ for (const key in models) {
+ return models[key].extraParams?.allFilter ?? '';
+ }
+ return '';
}
/**
@@ -81,10 +90,13 @@ export default class SelectionList {
* Keep in mind that when using CMD + A, we don't have all items in memory!
*/
get availableModels() {
+ const models = this.infinityModel;
const arr = [];
- for (const item of this.infinityModel.content) {
- if (this.isSelected(item.id)) {
- arr.push(item);
+ for (const key in models) {
+ for (const item of models[key].content) {
+ if (this.isSelected(item.id)) {
+ arr.push(item);
+ }
}
}
return arr;
@@ -102,7 +114,13 @@ export default class SelectionList {
if (!this.inverted) {
return this.selectedIds.size;
}
- return Math.max((this.infinityModel.meta?.pagination?.total ?? 0) - this.selectedIds.size, 1);
+
+ const models = this.infinityModel;
+ let total;
+ for (const key in models) {
+ total += models[key].meta?.pagination?.total;
+ }
+ return Math.max((total ?? 0) - this.selectedIds.size, 1);
}
isSelected(id) {
@@ -147,9 +165,12 @@ export default class SelectionList {
clearUnavailableItems() {
const newSelection = new Set();
- for (const item of this.infinityModel.content) {
- if (this.selectedIds.has(item.id)) {
- newSelection.add(item.id);
+ const models = this.infinityModel;
+ for (const key in models) {
+ for (const item of models[key].content) {
+ if (this.selectedIds.has(item.id)) {
+ newSelection.add(item.id);
+ }
}
}
this.selectedIds = newSelection;
@@ -181,37 +202,40 @@ export default class SelectionList {
// todo
let running = false;
- for (const item of this.infinityModel.content) {
- // Exlusing the last selected item
- if (item.id === this.lastSelectedId || item.id === id) {
- if (!running) {
- running = true;
+ const models = this.infinityModel;
+ for (const key in models) {
+ for (const item of this.models[key].content) {
+ // Exlusing the last selected item
+ if (item.id === this.lastSelectedId || item.id === id) {
+ if (!running) {
+ running = true;
- // Skip last selected on its own
- if (item.id === this.lastSelectedId) {
- continue;
- }
- } else {
- // Still include id
- if (item.id === id) {
- this.lastShiftSelectionGroup.add(item.id);
-
- if (this.inverted) {
- this.selectedIds.delete(item.id);
- } else {
- this.selectedIds.add(item.id);
+ // Skip last selected on its own
+ if (item.id === this.lastSelectedId) {
+ continue;
}
- }
- break;
- }
- }
+ } else {
+ // Still include id
+ if (item.id === id) {
+ this.lastShiftSelectionGroup.add(item.id);
- if (running) {
- this.lastShiftSelectionGroup.add(item.id);
- if (this.inverted) {
- this.selectedIds.delete(item.id);
- } else {
- this.selectedIds.add(item.id);
+ if (this.inverted) {
+ this.selectedIds.delete(item.id);
+ } else {
+ this.selectedIds.add(item.id);
+ }
+ }
+ break;
+ }
+ }
+
+ if (running) {
+ this.lastShiftSelectionGroup.add(item.id);
+ if (this.inverted) {
+ this.selectedIds.delete(item.id);
+ } else {
+ this.selectedIds.add(item.id);
+ }
}
}
}
diff --git a/ghost/admin/app/controllers/pages.js b/ghost/admin/app/controllers/pages.js
index 6a011d1564..cd62cc80b5 100644
--- a/ghost/admin/app/controllers/pages.js
+++ b/ghost/admin/app/controllers/pages.js
@@ -40,4 +40,4 @@ export default class PagesController extends PostsController {
openEditor(page) {
this.router.transitionTo('lexical-editor.edit', 'page', page.get('id'));
}
-}
+}
\ No newline at end of file
diff --git a/ghost/admin/app/controllers/posts.js b/ghost/admin/app/controllers/posts.js
index 014cad0f47..8f48fca2db 100644
--- a/ghost/admin/app/controllers/posts.js
+++ b/ghost/admin/app/controllers/posts.js
@@ -1,5 +1,5 @@
import Controller from '@ember/controller';
-import SelectionList from 'ghost-admin/utils/selection-list';
+import SelectionList from 'ghost-admin/components/posts-list/selection-list';
import {DEFAULT_QUERY_PARAMS} from 'ghost-admin/helpers/reset-query-params';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
@@ -85,14 +85,6 @@ export default class PostsController extends Controller {
Object.assign(this, DEFAULT_QUERY_PARAMS.posts);
}
- get postsInfinityModel() {
- return this.model;
- }
-
- get totalPosts() {
- return this.model.meta?.pagination?.total ?? 0;
- }
-
get showingAll() {
const {type, author, tag, visibility} = this;
diff --git a/ghost/admin/app/routes/posts.js b/ghost/admin/app/routes/posts.js
index 93e7d5d4ab..a1c11aac27 100644
--- a/ghost/admin/app/routes/posts.js
+++ b/ghost/admin/app/routes/posts.js
@@ -1,4 +1,5 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
+import RSVP from 'rsvp';
import {action} from '@ember/object';
import {assign} from '@ember/polyfills';
import {isBlank} from '@ember/utils';
@@ -39,43 +40,54 @@ export default class PostsRoute extends AuthenticatedRoute {
model(params) {
const user = this.session.user;
- let queryParams = {};
let filterParams = {tag: params.tag, visibility: params.visibility};
let paginationParams = {
perPageParam: 'limit',
totalPagesParam: 'meta.pagination.pages'
};
-
+
+ // type filters are actually mapping statuses
assign(filterParams, this._getTypeFilters(params.type));
-
+
if (params.type === 'featured') {
filterParams.featured = true;
}
-
+
+ // authors and contributors can only view their own posts
if (user.isAuthor) {
- // authors can only view their own posts
filterParams.authors = user.slug;
} else if (user.isContributor) {
- // Contributors can only view their own draft posts
filterParams.authors = user.slug;
- // filterParams.status = 'draft';
+ // otherwise we need to filter by author if present
} else if (params.author) {
filterParams.authors = params.author;
}
-
- let filter = this._filterString(filterParams);
- if (!isBlank(filter)) {
- queryParams.filter = filter;
- }
-
- if (!isBlank(params.order)) {
- queryParams.order = params.order;
- }
-
+
let perPage = this.perPage;
- let paginationSettings = assign({perPage, startingPage: 1}, paginationParams, queryParams);
+
+ const filterStatuses = filterParams.status;
+ let queryParams = {allFilter: this._filterString({...filterParams})}; // pass along the parent filter so it's easier to apply the params filter to each infinity model
+ let models = {};
- return this.infinity.model(this.modelName, paginationSettings);
+ if (filterStatuses.includes('scheduled')) {
+ let scheduledInfinityModelParams = {...queryParams, order: params.order || 'published_at desc', filter: this._filterString({...filterParams, status: 'scheduled'})};
+ models.scheduledInfinityModel = this.infinity.model(this.modelName, assign({perPage, startingPage: 1}, paginationParams, scheduledInfinityModelParams));
+ }
+ if (filterStatuses.includes('draft')) {
+ let draftInfinityModelParams = {...queryParams, order: params.order || 'updated_at desc', filter: this._filterString({...filterParams, status: 'draft'})};
+ models.draftInfinityModel = this.infinity.model(this.modelName, assign({perPage, startingPage: 1}, paginationParams, draftInfinityModelParams));
+ }
+ if (filterStatuses.includes('published') || filterStatuses.includes('sent')) {
+ let publishedAndSentInfinityModelParams;
+ if (filterStatuses.includes('published') && filterStatuses.includes('sent')) {
+ publishedAndSentInfinityModelParams = {...queryParams, order: params.order || 'published_at desc', filter: this._filterString({...filterParams, status: '[published,sent]'})};
+ } else {
+ publishedAndSentInfinityModelParams = {...queryParams, order: params.order || 'published_at desc', filter: this._filterString({...filterParams, status: filterStatuses.includes('published') ? 'published' : 'sent'})};
+ }
+ models.publishedAndSentInfinityModel = this.infinity.model(this.modelName, assign({perPage, startingPage: 1}, paginationParams, publishedAndSentInfinityModelParams));
+ }
+
+ return RSVP.hash(models);
}
// trigger a background load of all tags and authors for use in filter dropdowns
@@ -120,6 +132,12 @@ export default class PostsRoute extends AuthenticatedRoute {
};
}
+ /**
+ * Returns an object containing the status filter based on the given type.
+ *
+ * @param {string} type - The type of filter to generate (draft, published, scheduled, sent).
+ * @returns {Object} - An object containing the status filter.
+ */
_getTypeFilters(type) {
let status = '[draft,scheduled,published,sent]';
diff --git a/ghost/admin/app/templates/pages.hbs b/ghost/admin/app/templates/pages.hbs
index 723ebf8c17..7dba875970 100644
--- a/ghost/admin/app/templates/pages.hbs
+++ b/ghost/admin/app/templates/pages.hbs
@@ -28,7 +28,7 @@
@@ -41,7 +41,7 @@
{{else}}
No pages match the current filter
-
+
Show all pages
{{/if}}
@@ -49,11 +49,26 @@
+ {{!-- only show one infinity loader wheel at a time - always order as scheduled, draft, remainder --}}
+ {{#if @model.scheduledInfinityModel}}
-
+ {{/if}}
+ {{#if (and @model.draftInfinityModel (or (not @model.scheduledInfinityModel) (and @model.scheduledInfinityModel @model.scheduledInfinityModel.reachedInfinity)))}}
+
+ {{/if}}
+ {{#if (and @model.publishedAndSentInfinityModel (and (or (not @model.scheduledInfinityModel) @model.scheduledInfinityModel.reachedInfinity) (or (not @model.draftInfinityModel) @model.draftInfinityModel.reachedInfinity)))}}
+
+ {{/if}}
+
{{outlet}}
diff --git a/ghost/admin/app/templates/posts.hbs b/ghost/admin/app/templates/posts.hbs
index f0d0b6bbe8..3d99d9ee66 100644
--- a/ghost/admin/app/templates/posts.hbs
+++ b/ghost/admin/app/templates/posts.hbs
@@ -30,7 +30,7 @@
@@ -43,7 +43,7 @@
{{else}}
No posts match the current filter
-
+
Show all posts
{{/if}}
@@ -51,11 +51,26 @@
+ {{!-- only show one infinity loader wheel at a time - always order as scheduled, draft, remainder --}}
+ {{#if @model.scheduledInfinityModel}}
-
+ {{/if}}
+ {{#if (and @model.draftInfinityModel (or (not @model.scheduledInfinityModel) (and @model.scheduledInfinityModel @model.scheduledInfinityModel.reachedInfinity)))}}
+
+ {{/if}}
+ {{#if (and @model.publishedAndSentInfinityModel (and (or (not @model.scheduledInfinityModel) @model.scheduledInfinityModel.reachedInfinity) (or (not @model.draftInfinityModel) @model.draftInfinityModel.reachedInfinity)))}}
+
+ {{/if}}
+
{{outlet}}
diff --git a/ghost/admin/mirage/config/pages.js b/ghost/admin/mirage/config/pages.js
index 9ab0162c06..6faeb2b44a 100644
--- a/ghost/admin/mirage/config/pages.js
+++ b/ghost/admin/mirage/config/pages.js
@@ -37,7 +37,6 @@ export default function mockPages(server) {
return pages.create(attrs);
});
- // TODO: handle authors filter
server.get('/pages/', function ({pages}, {queryParams}) {
let {filter, page, limit} = queryParams;
diff --git a/ghost/admin/mirage/config/posts.js b/ghost/admin/mirage/config/posts.js
index a12863bfe7..2836e0613d 100644
--- a/ghost/admin/mirage/config/posts.js
+++ b/ghost/admin/mirage/config/posts.js
@@ -23,7 +23,6 @@ function extractTags(postAttrs, tags) {
});
}
-// TODO: handle authors filter
export function getPosts({posts}, {queryParams}) {
let {filter, page, limit} = queryParams;
@@ -31,15 +30,27 @@ export function getPosts({posts}, {queryParams}) {
limit = +limit || 15;
let statusFilter = extractFilterParam('status', filter);
+ let authorsFilter = extractFilterParam('authors', filter);
+ let visibilityFilter = extractFilterParam('visibility', filter);
let collection = posts.all().filter((post) => {
let matchesStatus = true;
+ let matchesAuthors = true;
+ let matchesVisibility = true;
if (!isEmpty(statusFilter)) {
matchesStatus = statusFilter.includes(post.status);
}
- return matchesStatus;
+ if (!isEmpty(authorsFilter)) {
+ matchesAuthors = authorsFilter.includes(post.authors.models[0].slug);
+ }
+
+ if (!isEmpty(visibilityFilter)) {
+ matchesVisibility = visibilityFilter.includes(post.visibility);
+ }
+
+ return matchesStatus && matchesAuthors && matchesVisibility;
});
return paginateModelCollection('posts', collection, page, limit);
@@ -59,7 +70,6 @@ export default function mockPosts(server) {
return posts.create(attrs);
});
- // TODO: handle authors filter
server.get('/posts/', getPosts);
server.get('/posts/:id/', function ({posts}, {params}) {
@@ -100,6 +110,13 @@ export default function mockPosts(server) {
posts.find(ids).destroy();
});
+ server.post('/posts/:id/copy/', function ({posts}, {params}) {
+ let post = posts.find(params.id);
+ let attrs = post.attrs;
+
+ return posts.create(attrs);
+ });
+
server.put('/posts/bulk/', function ({tags}, {requestBody}) {
const bulk = JSON.parse(requestBody).bulk;
const action = bulk.action;
@@ -115,7 +132,7 @@ export default function mockPosts(server) {
tags.create(tag);
}
});
- // TODO: update the actual posts in the mock db
+ // TODO: update the actual posts in the mock db if wanting to write tests where we navigate around (refresh model)
// const postsToUpdate = posts.find(ids);
// getting the posts is fine, but within this we CANNOT manipulate them (???) not even iterate with .forEach
}
diff --git a/ghost/admin/tests/acceptance/content-test.js b/ghost/admin/tests/acceptance/content-test.js
index 5fbbf3f743..2690dfc655 100644
--- a/ghost/admin/tests/acceptance/content-test.js
+++ b/ghost/admin/tests/acceptance/content-test.js
@@ -17,7 +17,9 @@ const findButton = (text, buttons) => {
return Array.from(buttons).find(button => button.innerText.trim() === text);
};
-describe('Acceptance: Content', function () {
+// NOTE: With accommodations for faster loading of posts in the UI, the requests to fetch the posts have been split into separate requests based
+// on the status of the post. This means that the tests for filtering by status will have multiple requests to check against.
+describe('Acceptance: Posts / Pages', function () {
let hooks = setupApplicationTest();
setupMirage(hooks);
@@ -25,550 +27,643 @@ describe('Acceptance: Content', function () {
this.server.loadFixtures('configs');
});
- it('redirects to signin when not authenticated', async function () {
- await invalidateSession();
- await visit('/posts');
-
- expect(currentURL()).to.equal('/signin');
- });
-
- describe('as admin', function () {
- let admin, editor, publishedPost, scheduledPost, draftPost, authorPost;
-
- 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 = 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', visibiity: 'paid'});
-
- // pages shouldn't appear in the list
- this.server.create('page', {authors: [admin], status: 'published', title: 'Published Page'});
-
- return await authenticateSession();
+ describe('posts', function () {
+ it('redirects to signin when not authenticated', async function () {
+ await invalidateSession();
+
+ await visit('/posts');
+ expect(currentURL()).to.equal('/signin');
});
- describe('displays and filter posts', function () {
- it('displays posts', async function () {
- await visit('/posts');
+ describe('as contributor', function () {
+ beforeEach(async function () {
+ let contributorRole = this.server.create('role', {name: 'Contributor'});
+ this.server.create('user', {roles: [contributorRole]});
- 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);
-
- // note: atm the mirage backend doesn't support ordering of the results set
+ return await authenticateSession();
});
- it('can filter by status', async function () {
+ // NOTE: This test seems to fail if run AFTER the 'can change access' test in the 'as admin' section; router seems to fail, did not look into it further
+ it('shows posts list and allows post creation', async function () {
await visit('/posts');
- // show draft posts
- await selectChoose('[data-test-type-select]', 'Draft posts');
+ // has an empty state
+ expect(findAll('[data-test-post-id]')).to.have.length(0);
+ expect(find('[data-test-no-posts-box]')).to.exist;
+ expect(find('[data-test-link="write-a-new-post"]')).to.exist;
+
+ await click('[data-test-link="write-a-new-post"]');
+
+ expect(currentURL()).to.equal('/editor/post');
+
+ await fillIn('[data-test-editor-title-input]', 'First contributor post');
+ await blur('[data-test-editor-title-input]');
+
+ expect(currentURL()).to.equal('/editor/post/1');
+
+ await click('[data-test-link="posts"]');
+
+ expect(findAll('[data-test-post-id]')).to.have.length(1);
+ expect(find('[data-test-no-posts-box]')).to.not.exist;
+ });
+ });
+
+ describe('as author', function () {
+ let author, authorPost;
+
+ 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 = this.server.create('post', {authors: [author], status: 'published', title: 'Author Post'});
+ this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
+
+ return await authenticateSession();
+ });
+
+ it('only fetches the author\'s posts', async function () {
+ await visit('/posts');
+ // trigger a filter request so we can grab the posts API request easily
+ await selectChoose('[data-test-type-select]', 'Published posts');
+
+ // API request includes author filter
+ 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(findAll('[data-test-post-id]').length, 'post count').to.equal(1);
+ expect(find(`[data-test-post-id="${authorPost.id}"]`), 'author post').to.exist;
+ });
+ });
+
+ describe('as admin', function () {
+ let admin, editor, publishedPost, scheduledPost, draftPost, authorPost;
+
+ 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 = this.server.create('post', {authors: [admin], status: 'published', title: 'Published Post', visibility: 'paid'});
+ scheduledPost = this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Scheduled Post'});
+ 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'});
+
+ // pages shouldn't appear in the list
+ this.server.create('page', {authors: [admin], status: 'published', title: 'Published Page'});
+
+ return await authenticateSession();
+ });
+
+ describe('displays and filter posts', function () {
+ it('displays posts', async function () {
+ await visit('/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);
+
+ // make sure display is scheduled > draft > published/sent
+ expect(posts[0].querySelector('.gh-content-entry-title').textContent, 'post 1 title').to.contain('Scheduled Post');
+ expect(posts[1].querySelector('.gh-content-entry-title').textContent, 'post 2 title').to.contain('Draft Post');
+ expect(posts[2].querySelector('.gh-content-entry-title').textContent, 'post 3 title').to.contain('Published Post');
+ expect(posts[3].querySelector('.gh-content-entry-title').textContent, 'post 4 title').to.contain('Editor Published Post');
+
+ // check API requests
+ let lastRequests = this.server.pretender.handledRequests.filter(request => request.url.includes('/posts/'));
+ expect(lastRequests[0].queryParams.filter, 'scheduled request filter').to.have.string('status:scheduled');
+ expect(lastRequests[1].queryParams.filter, 'drafts request filter').to.have.string('status:draft');
+ expect(lastRequests[2].queryParams.filter, 'published request filter').to.have.string('status:[published,sent]');
+ });
+
+ it('can filter by status', async function () {
+ await visit('/posts');
+
+ // show draft posts
+ await selectChoose('[data-test-type-select]', 'Draft 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;
+ });
+
+ it('can filter by author', async function () {
+ await visit('/posts');
+
+ // show all posts by editor
+ await selectChoose('[data-test-author-select]', editor.name);
+
+ // API request is correct
+ let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
+ expect(lastRequest.queryParams.allFilter, '"editor" request status filter')
+ .to.have.string('status:[draft,scheduled,published,sent]');
+ expect(lastRequest.queryParams.allFilter, '"editor" request filter param')
+ .to.have.string(`authors:${editor.slug}`);
+
+ // Displays editor post
+ expect(findAll('[data-test-post-id]').length, 'editor count').to.equal(1);
+ });
+
+ it('can filter by visibility', async function () {
+ await visit('/posts');
+
+ await selectChoose('[data-test-visibility-select]', 'Paid members-only');
+ let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
+ expect(lastRequest.queryParams.allFilter, '"visibility" request filter param')
+ .to.have.string('visibility:[paid,tiers]');
+ let posts = findAll('[data-test-post-id]');
+ expect(posts.length, 'all posts count').to.equal(1);
+
+ await selectChoose('[data-test-visibility-select]', 'Public');
+ [lastRequest] = this.server.pretender.handledRequests.slice(-1);
+ expect(lastRequest.queryParams.allFilter, '"visibility" request filter param')
+ .to.have.string('visibility:public');
+ posts = findAll('[data-test-post-id]');
+ expect(posts.length, 'all posts count').to.equal(3);
+ });
+
+ 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'});
+
+ await visit('/posts');
+ await clickTrigger('[data-test-tag-select]');
+
+ 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.allFilter, '"tag" request filter param').to.have.string('tag:second');
+ });
+ });
+
+ describe('context menu actions', function () {
+ describe('single post', function () {
+ it('can duplicate a post', async function () {
+ await visit('/posts');
+
+ // get the post
+ const post = find(`[data-test-post-id="${publishedPost.id}"]`);
+ expect(post, 'post').to.exist;
+
+ await triggerEvent(post, 'contextmenu');
+
+ let contextMenu = find('.gh-posts-context-menu'); // this is a
element
+
+ let buttons = contextMenu.querySelectorAll('button');
+
+ 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]);
+
+ const posts = findAll('[data-test-post-id]');
+ expect(posts.length, 'all posts count').to.equal(5);
+ let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
+ expect(lastRequest.url, 'request url').to.match(new RegExp(`/posts/${publishedPost.id}/copy/`));
+ });
+ });
+
+ describe('multiple posts', function () {
+ it('can feature and unfeature', 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 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:['${publishedPost.id}','${authorPost.id}']`);
+ 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 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:['${publishedPost.id}','${authorPost.id}']`);
+ 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', 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 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:['${publishedPost.id}','${authorPost.id}']`);
+ expect(JSON.parse(lastRequest.requestBody).bulk.action, 'add tag request action').to.equal('addTag');
+ });
+
+ // TODO: Skip for now. This causes the member creation test to fail ('New member' text doesn't show... ???).
+ it.skip('can change access', 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;
+
+ await triggerEvent(postFourContainer, 'contextmenu');
+ let contextMenu = find('.gh-posts-context-menu'); // this is a element
+ expect(contextMenu, 'context menu').to.exist;
+ let buttons = contextMenu.querySelectorAll('button');
+ let changeAccessButton = findButton('Change access', buttons);
+
+ expect(changeAccessButton, 'change access button').not.to.exist;
+
+ const settingsService = this.owner.lookup('service:settings');
+ await settingsService.set('membersEnabled', true);
+
+ await triggerEvent(postFourContainer, 'contextmenu');
+ contextMenu = find('.gh-posts-context-menu'); // this is a element
+ expect(contextMenu, 'context menu').to.exist;
+ buttons = contextMenu.querySelectorAll('button');
+ changeAccessButton = findButton('Change access', buttons);
+
+ expect(changeAccessButton, 'change access button').to.exist;
+ await click(changeAccessButton);
+
+ const changeAccessModal = find('[data-test-modal="edit-posts-access"]');
+ const selectElement = changeAccessModal.querySelector('select');
+ await fillIn(selectElement, 'members');
+ await click('[data-test-button="confirm"]');
+
+ // check API request
+ let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
+ expect(lastRequest.queryParams.filter, 'change access request id').to.equal(`id:['${publishedPost.id}','${authorPost.id}']`);
+ expect(JSON.parse(lastRequest.requestBody).bulk.action, 'change access request action').to.equal('access');
+ });
+
+ it('can unpublish', 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 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:['${publishedPost.id}','${authorPost.id}']`);
+ 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', 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 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:['${publishedPost.id}','${authorPost.id}']`);
+ 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 () {
+ // actions are not visible when there's no filter
+ await visit('/posts');
+ expect(find('[data-test-button="edit-view"]'), 'edit-view button (no filter)').to.not.exist;
+ expect(find('[data-test-button="add-view"]'), 'add-view button (no filter)').to.not.exist;
+
+ // add action is visible after filtering to a non-default filter
+ await selectChoose('[data-test-author-select]', admin.name);
+ expect(find('[data-test-button="add-view"]'), 'add-view button (with filter)').to.exist;
+
+ // adding view shows it in the sidebar
+ await click('[data-test-button="add-view"]'), 'add-view button';
+ expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (on add)').to.exist;
+ expect(find('[data-test-modal="custom-view-form"] h1').textContent.trim()).to.equal('New view');
+ await fillIn('[data-test-input="custom-view-name"]', 'Test view');
+ await click('[data-test-button="save-custom-view"]');
+ // modal closes on save
+ expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (after add save)').to.not.exist;
+ // UI updates
+ expect(find('[data-test-nav-custom="posts-Test view"]'), 'new view nav').to.exist;
+ expect(find('[data-test-nav-custom="posts-Test view"]').textContent.trim()).to.equal('Test view');
+ expect(find('[data-test-button="add-view"]'), 'add-view button (on existing view)').to.not.exist;
+ expect(find('[data-test-button="edit-view"]'), 'edit-view button (on existing view)').to.exist;
+
+ // editing view
+ await click('[data-test-button="edit-view"]'), 'edit-view button';
+ expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (on edit)').to.exist;
+ expect(find('[data-test-modal="custom-view-form"] h1').textContent.trim()).to.equal('Edit view');
+ await fillIn('[data-test-input="custom-view-name"]', 'Updated view');
+ await click('[data-test-button="save-custom-view"]');
+ // modal closes on save
+ expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (after edit save)').to.not.exist;
+ // UI updates
+ expect(find('[data-test-nav-custom="posts-Updated view"]')).to.exist;
+ expect(find('[data-test-nav-custom="posts-Updated view"]').textContent.trim()).to.equal('Updated view');
+ expect(find('[data-test-button="add-view"]'), 'add-view button (after edit)').to.not.exist;
+ expect(find('[data-test-button="edit-view"]'), 'edit-view button (after edit)').to.exist;
+ });
+
+ it('can navigate to custom views', async function () {
+ this.server.create('setting', {
+ group: 'site',
+ key: 'shared_views',
+ value: JSON.stringify([{
+ route: 'posts',
+ name: 'My posts',
+ filter: {
+ author: admin.slug
+ }
+ }])
+ });
+
+ await visit('/posts');
+
+ // nav bar contains default + custom views
+ expect(find('[data-test-nav-custom="posts-Drafts"]')).to.exist;
+ expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.exist;
+ expect(find('[data-test-nav-custom="posts-Published"]')).to.exist;
+ expect(find('[data-test-nav-custom="posts-My posts"]')).to.exist;
+
+ // screen has default title and sidebar is showing inactive custom view
+ expect(find('[data-test-screen-title]')).to.have.rendered.text('Posts');
+ expect(find('[data-test-nav="posts"]')).to.have.class('active');
+
+ // clicking sidebar custom view link works
+ await click('[data-test-nav-custom="posts-Scheduled"]');
+ expect(currentURL()).to.equal('/posts?type=scheduled');
+ expect(find('[data-test-screen-title]').innerText).to.match(/Scheduled/);
+ expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.have.class('active');
+
+ // clicking the main posts link resets
+ await click('[data-test-nav="posts"]');
+ expect(currentURL()).to.equal('/posts');
+ expect(find('[data-test-screen-title]')).to.have.rendered.text('Posts');
+ expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.not.have.class('active');
+
+ // changing a filter to match a custom view shows custom view
+ await selectChoose('[data-test-type-select]', 'Scheduled posts');
+ expect(currentURL()).to.equal('/posts?type=scheduled');
+ expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.have.class('active');
+ expect(find('[data-test-screen-title]').innerText).to.match(/Scheduled/);
+ });
+ });
+ });
+
+ // NOTE: Because the pages list is (at this point in time) a thin extension of the posts list, we should not need to duplicate all of the tests.
+ // The main difference is that we fetch pages, not posts.
+ // IF we implement any kind of functionality that *is* specific to a post or page and differentiate these models further, we will need to add tests then.
+ describe('pages', function () {
+ describe('as admin', function () {
+ let admin, editor;
+
+ 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]});
+
+ // posts shouldn't show in the pages list
+ // TODO: figure out why we need post counts to be >= page count for mirage to work right
+ this.server.create('post', {authors: [admin], status: 'published', title: 'Published Post', visibility: 'paid'});
+ this.server.create('post', {authors: [admin], status: 'published', title: 'Published Post', visibility: 'paid'});
+ this.server.create('post', {authors: [admin], status: 'published', title: 'Published Post', visibility: 'paid'});
+ this.server.create('post', {authors: [admin], status: 'published', title: 'Published Post', visibility: 'paid'});
+
+ this.server.create('page', {authors: [admin], status: 'published', title: 'Published Page'});
+ this.server.create('page', {authors: [editor], status: 'published', title: 'Editor Published Page'});
+ this.server.create('page', {authors: [admin], status: 'draft', title: 'Draft Page'});
+ this.server.create('page', {authors: [admin], status: 'scheduled', title: 'Scheduled Page'});
+
+ return await authenticateSession();
+ });
+
+ it('can view pages', async function () {
+ await visit('/pages');
+
+ const pages = findAll('[data-test-post-id]');
+ // displays all pages by default (all statuses)
+ expect(pages.length, 'all pages count').to.equal(4);
+ });
+
+ it('can filter pages', async function () {
+ await visit('/pages');
+
+ // show draft pages
+ await selectChoose('[data-test-type-select]', 'Draft pages');
// 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
+ // Displays draft page
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');
-
+ expect(find('[data-test-post-id="3"]'), 'draft page').to.exist;
+
+ // show published pages
+ await selectChoose('[data-test-type-select]', 'Published pages');
+
// 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
+ // Displays two published 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');
-
+ expect(find('[data-test-post-id="1"]'), 'admin published page').to.exist;
+ expect(find('[data-test-post-id="2"]'), 'editor published page').to.exist;
+
+ // show scheduled pages
+ await selectChoose('[data-test-type-select]', 'Scheduled pages');
+
// 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
+ // Displays scheduled page
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]');
+ expect(find('[data-test-post-id="4"]'), 'scheduled page').to.exist;
});
-
- it('can filter by author', async function () {
- await visit('/posts');
-
- // show all posts by editor
- await selectChoose('[data-test-author-select]', editor.name);
-
- // 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}`);
- });
-
- it('can filter by visibility', async function () {
- await visit('/posts');
-
- await selectChoose('[data-test-visibility-select]', 'Paid members-only');
-
- 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]');
- });
-
- 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'});
-
- await visit('/posts');
- await clickTrigger('[data-test-tag-select]');
-
- 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');
- });
- });
-
- describe('context menu actions', function () {
- describe('single post', function () {
- // has a duplicate option
- it.skip('can duplicate a post', async function () {
- await visit('/posts');
-
- // get the post
- const post = find(`[data-test-post-id="${publishedPost.id}"]`);
- expect(post, 'post').to.exist;
-
- await triggerEvent(post, 'contextmenu');
- // await this.pauseTest();
-
- let contextMenu = find('.gh-posts-context-menu'); // this is a 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 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 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 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 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 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 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 () {
- // actions are not visible when there's no filter
- await visit('/posts');
- expect(find('[data-test-button="edit-view"]'), 'edit-view button (no filter)').to.not.exist;
- expect(find('[data-test-button="add-view"]'), 'add-view button (no filter)').to.not.exist;
-
- // add action is visible after filtering to a non-default filter
- await selectChoose('[data-test-author-select]', admin.name);
- expect(find('[data-test-button="add-view"]'), 'add-view button (with filter)').to.exist;
-
- // adding view shows it in the sidebar
- await click('[data-test-button="add-view"]'), 'add-view button';
- expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (on add)').to.exist;
- expect(find('[data-test-modal="custom-view-form"] h1').textContent.trim()).to.equal('New view');
- await fillIn('[data-test-input="custom-view-name"]', 'Test view');
- await click('[data-test-button="save-custom-view"]');
- // modal closes on save
- expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (after add save)').to.not.exist;
- // UI updates
- expect(find('[data-test-nav-custom="posts-Test view"]'), 'new view nav').to.exist;
- expect(find('[data-test-nav-custom="posts-Test view"]').textContent.trim()).to.equal('Test view');
- expect(find('[data-test-button="add-view"]'), 'add-view button (on existing view)').to.not.exist;
- expect(find('[data-test-button="edit-view"]'), 'edit-view button (on existing view)').to.exist;
-
- // editing view
- await click('[data-test-button="edit-view"]'), 'edit-view button';
- expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (on edit)').to.exist;
- expect(find('[data-test-modal="custom-view-form"] h1').textContent.trim()).to.equal('Edit view');
- await fillIn('[data-test-input="custom-view-name"]', 'Updated view');
- await click('[data-test-button="save-custom-view"]');
- // modal closes on save
- expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (after edit save)').to.not.exist;
- // UI updates
- expect(find('[data-test-nav-custom="posts-Updated view"]')).to.exist;
- expect(find('[data-test-nav-custom="posts-Updated view"]').textContent.trim()).to.equal('Updated view');
- expect(find('[data-test-button="add-view"]'), 'add-view button (after edit)').to.not.exist;
- expect(find('[data-test-button="edit-view"]'), 'edit-view button (after edit)').to.exist;
- });
-
- it('can navigate to custom views', async function () {
- this.server.create('setting', {
- group: 'site',
- key: 'shared_views',
- value: JSON.stringify([{
- route: 'posts',
- name: 'My posts',
- filter: {
- author: admin.slug
- }
- }])
- });
-
- await visit('/posts');
-
- // nav bar contains default + custom views
- expect(find('[data-test-nav-custom="posts-Drafts"]')).to.exist;
- expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.exist;
- expect(find('[data-test-nav-custom="posts-Published"]')).to.exist;
- expect(find('[data-test-nav-custom="posts-My posts"]')).to.exist;
-
- // screen has default title and sidebar is showing inactive custom view
- expect(find('[data-test-screen-title]')).to.have.rendered.text('Posts');
- expect(find('[data-test-nav="posts"]')).to.have.class('active');
-
- // clicking sidebar custom view link works
- await click('[data-test-nav-custom="posts-Scheduled"]');
- expect(currentURL()).to.equal('/posts?type=scheduled');
- expect(find('[data-test-screen-title]').innerText).to.match(/Scheduled/);
- expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.have.class('active');
-
- // clicking the main posts link resets
- await click('[data-test-nav="posts"]');
- expect(currentURL()).to.equal('/posts');
- expect(find('[data-test-screen-title]')).to.have.rendered.text('Posts');
- expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.not.have.class('active');
-
- // changing a filter to match a custom view shows custom view
- await selectChoose('[data-test-type-select]', 'Scheduled posts');
- expect(currentURL()).to.equal('/posts?type=scheduled');
- expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.have.class('active');
- expect(find('[data-test-screen-title]').innerText).to.match(/Scheduled/);
- });
- });
-
- describe('as author', function () {
- let author, authorPost;
-
- 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 = this.server.create('post', {authors: [author], status: 'published', title: 'Author Post'});
- this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
-
- return await authenticateSession();
- });
-
- it('only fetches the author\'s posts', async function () {
- await visit('/posts');
- // trigger a filter request so we can grab the posts API request easily
- await selectChoose('[data-test-type-select]', 'Published posts');
-
- // API request includes author filter
- 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(findAll('[data-test-post-id]').length, 'post count').to.equal(1);
- expect(find(`[data-test-post-id="${authorPost.id}"]`), 'author post').to.exist;
- });
- });
-
- describe('as contributor', function () {
- beforeEach(async function () {
- let contributorRole = this.server.create('role', {name: 'Contributor'});
- this.server.create('user', {roles: [contributorRole]});
-
- return await authenticateSession();
- });
-
- it('shows posts list and allows post creation', async function () {
- await visit('/posts');
-
- // has an empty state
- expect(findAll('[data-test-post-id]')).to.have.length(0);
- expect(find('[data-test-no-posts-box]')).to.exist;
- expect(find('[data-test-link="write-a-new-post"]')).to.exist;
-
- await click('[data-test-link="write-a-new-post"]');
-
- expect(currentURL()).to.equal('/editor/post');
-
- await fillIn('[data-test-editor-title-input]', 'First contributor post');
- await blur('[data-test-editor-title-input]');
-
- expect(currentURL()).to.equal('/editor/post/1');
-
- await click('[data-test-link="posts"]');
-
- expect(findAll('[data-test-post-id]')).to.have.length(1);
- expect(find('[data-test-no-posts-box]')).to.not.exist;
});
});
});
\ No newline at end of file