0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Fixed comments-ui tests not properly mocking actions on replies

no issue

- `MockedApi.browseComments` only worked on top-level comments, it couldn't find replies when passed `filter:'id:123'` such is used when liking and editing replies
- fixed missing get handler for fetching single comment via Admin API (used after showing a hidden comment)
- added `flattenComments` and `findCommentById` helper functions to facilitate easier finding of a comment or reply within the nested structure
- added tests for liking and hiding replies
This commit is contained in:
Kevin Ansfield 2024-11-26 18:28:53 +00:00
parent d32955b21e
commit 04f337e085
5 changed files with 124 additions and 8 deletions

View file

@ -3,6 +3,34 @@ import moment, {DurationInputObject} from 'moment';
import sinon from 'sinon';
import {buildAnonymousMember, buildComment, buildDeletedMember} from '../../test/utils/fixtures';
/* eslint-disable @typescript-eslint/no-explicit-any */
describe('flattenComments', function () {
it('flattens comments and replies', function () {
const comments: any[] = [
{id: '1', replies: [{id: '2'}]},
{id: '3', replies: []}
];
expect(helpers.flattenComments(comments)).toEqual([
{id: '1', replies: [{id: '2'}]},
{id: '2'},
{id: '3', replies: []}
]);
});
});
describe('findCommentById', function () {
it('finds a top-level comment', function () {
const comments: any[] = [{id: '1'}, {id: '2'}, {id: '3'}];
expect(helpers.findCommentById(comments, '2')).toEqual({id: '2'});
});
it('finds a reply', function () {
const comments: any[] = [{id: '1', replies: [{id: '2'}]}, {id: '3'}];
expect(helpers.findCommentById(comments, '2')).toEqual({id: '2'});
});
});
describe('formatNumber', function () {
it('adds commas to large numbers', function () {
expect(helpers.formatNumber(1234567)).toEqual('1,234,567');

View file

@ -1,5 +1,14 @@
import {Comment, Member, TranslationFunction} from '../AppContext';
export function flattenComments(comments: Comment[]): Comment[] {
return comments.flatMap(comment => [comment, ...(comment.replies || [])]);
}
export function findCommentById(comments: Comment[], id: string): Comment | undefined {
return comments.find(comment => comment?.id === id)
|| comments.flatMap(comment => comment.replies || []).find(reply => reply?.id === id);
}
export function formatNumber(number: number): string {
if (number !== 0 && !number) {
return '';

View file

@ -1,4 +1,5 @@
import {MockedApi, initialize, waitEditorFocused} from '../utils/e2e';
import {buildReply} from '../utils/fixtures';
import {expect, test} from '@playwright/test';
test.describe('Actions', async () => {
@ -72,6 +73,29 @@ test.describe('Actions', async () => {
await expect(likeButton2).toHaveText('52');
});
test('Can like and unlike a reply', async ({page}) => {
mockedApi.addComment({
id: '1',
html: '<p>This is comment 1</p>',
replies: [
buildReply({id: '2', html: '<p>This is reply 1</p>'}),
buildReply({id: '3', html: '<p>This is reply 2</p>', in_reply_to_id: '2', in_reply_to_snippet: 'This is reply 1'}),
buildReply({id: '4', html: '<p>This is reply 3</p>'})
]
});
const {frame} = await initializeTest(page);
const reply = frame.getByTestId('comment-component').nth(1);
const likeButton = reply.getByTestId('like-button');
await expect(likeButton).toHaveText('0');
await likeButton.click();
await expect(likeButton).toHaveText('1');
await likeButton.click();
await expect(likeButton).toHaveText('0');
});
test('Can reply to a comment', async ({page}) => {
mockedApi.addComment({
html: '<p>This is comment 1</p>'

View file

@ -1,5 +1,6 @@
import sinon from 'sinon';
import {MOCKED_SITE_URL, MockedApi, initialize, mockAdminAuthFrame, mockAdminAuthFrame204} from '../utils/e2e';
import {buildReply} from '../utils/fixtures';
import {expect, test} from '@playwright/test';
const admin = MOCKED_SITE_URL + '/ghost/';
@ -169,5 +170,32 @@ test.describe('Admin moderation', async () => {
await moreButtons.nth(1).getByText('Show comment').click();
await expect(secondComment).toContainText('This is comment 2');
});
test('can hide and show replies', async ({page}) => {
mockedApi.addComment({
id: '1',
html: '<p>This is comment 1</p>',
replies: [
buildReply({id: '2', html: '<p>This is reply 1</p>'}),
buildReply({id: '3', html: '<p>This is reply 2</p>'})
]
});
const {frame} = await initializeTest(page, {labs: true});
const comments = await frame.getByTestId('comment-component');
const replyToHide = comments.nth(1);
// Hide the 1st reply
await replyToHide.getByTestId('more-button').click();
await replyToHide.getByTestId('hide-button').click();
await expect(replyToHide).toContainText('Hidden for members');
// Show it again
await replyToHide.getByTestId('more-button').click();
await replyToHide.getByTestId('show-button').click();
await expect(replyToHide).not.toContainText('Hidden for members');
});
});
});

View file

@ -1,5 +1,6 @@
import nql from '@tryghost/nql';
import {buildComment, buildMember, buildReply, buildSettings} from './fixtures';
import {findCommentById, flattenComments} from '../../src/utils/helpers';
// The test file doesn't run in the browser, so we can't use the DOM API.
// We can use a simple regex to strip HTML tags from a string for test purposes.
@ -171,6 +172,13 @@ export class MockedApi {
// Parse NQL filter
if (filter) {
const parsed = nql(filter);
// When fetching a comment by ID, we need to include all replies so
// the quick way to do that is to flatten the comment+replies array
if (filter.includes('id:')) {
filteredComments = flattenComments(filteredComments);
}
filteredComments = filteredComments.filter((comment) => {
return parsed.queryJSON(comment);
});
@ -182,14 +190,13 @@ export class MockedApi {
const comments = filteredComments.slice(startIndex, endIndex);
return {
comments: comments.map((comment) => {
return {
...comment,
replies: comment.replies.slice(0, 3),
replies: comment.replies ? comment.replies?.slice(0, 3) : [],
count: {
...comment.count,
replies: comment.replies.length
replies: comment.replies ? comment.replies?.length : 0
}
};
}),
@ -341,7 +348,7 @@ export class MockedApi {
const url = new URL(route.request().url());
const commentId = url.pathname.split('/').reverse()[2];
const comment = this.comments.find(c => c.id === commentId);
const comment = flattenComments(this.comments).find(c => c.id === commentId);
if (!comment) {
return await route.fulfill({
status: 404,
@ -441,14 +448,33 @@ export class MockedApi {
});
},
async updateComment(route) {
async getOrUpdateComment(route) {
await this.#delayResponse();
const url = new URL(route.request().url());
if (route.request().method() === 'GET') {
const commentId = url.pathname.split('/').reverse()[1];
await route.fulfill({
status: 200,
body: JSON.stringify(this.browseComments({
limit: 1,
filter: `id:'${commentId}'`,
page: 1,
order: '',
admin: true
}))
});
}
if (route.request().method() === 'PUT') {
const commentId = url.pathname.split('/').reverse()[1];
const payload = JSON.parse(route.request().postData());
const comment = this.comments.find(c => c.id === commentId);
const comment = findCommentById(this.comments, commentId);
if (!comment) {
await route.fulfill({status: 404});
return;
}
comment.status = payload.status;
@ -458,7 +484,8 @@ export class MockedApi {
limit: 1,
filter: `id:'${commentId}'`,
page: 1,
order: ''
order: '',
admin: true
}))
});
}
@ -479,6 +506,6 @@ export class MockedApi {
// Admin API -----------------------------------------------------------
await page.route(`${path}/ghost/api/admin/users/me/`, this.adminRequestHandlers.getUser.bind(this));
await page.route(`${path}/ghost/api/admin/comments/post/*/*`, this.adminRequestHandlers.browseComments.bind(this));
await page.route(`${path}/ghost/api/admin/comments/*/`, this.adminRequestHandlers.updateComment.bind(this));
await page.route(`${path}/ghost/api/admin/comments/*/`, this.adminRequestHandlers.getOrUpdateComment.bind(this));
}
}