mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Moved old comment tests to Playwright (#17750)
refs https://github.com/TryGhost/Product/issues/3504
This commit is contained in:
parent
b388e392ff
commit
850cc7a9a1
7 changed files with 206 additions and 383 deletions
|
@ -1,377 +0,0 @@
|
|||
import App from './App';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {ROOT_DIV_ID} from './utils/constants';
|
||||
import {act, fireEvent, render, waitFor, within} from '@testing-library/react';
|
||||
import {buildComment, buildMember} from '../test/utils/fixtures';
|
||||
|
||||
function renderApp({member = null, documentStyles = {}, props = {}} = {}) {
|
||||
const postId = 'my-post';
|
||||
const api = {
|
||||
init: async () => {
|
||||
return {
|
||||
member
|
||||
};
|
||||
},
|
||||
comments: {
|
||||
count: async () => {
|
||||
return {
|
||||
[postId]: 0
|
||||
};
|
||||
},
|
||||
browse: async () => {
|
||||
return {
|
||||
comments: [],
|
||||
meta: {
|
||||
pagination: {
|
||||
limit: 5,
|
||||
total: 0,
|
||||
next: null,
|
||||
prev: null,
|
||||
page: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
add: async ({comment}) => {
|
||||
return {
|
||||
comments: [
|
||||
{
|
||||
...buildComment(),
|
||||
...comment,
|
||||
member,
|
||||
replies: [],
|
||||
liked: false,
|
||||
count: {
|
||||
likes: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
replies: async () => {
|
||||
return {
|
||||
comments: [],
|
||||
meta: {
|
||||
pagination: {
|
||||
limit: 3,
|
||||
total: 0,
|
||||
next: null,
|
||||
prev: null,
|
||||
page: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
like: async () => {
|
||||
// noop
|
||||
},
|
||||
unlike: async () => {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
};
|
||||
// In tests, we currently don't wait for the styles to have loaded. In the app we check if the styles url is set or not.
|
||||
const {container} = render(<div style={documentStyles}><div id={ROOT_DIV_ID}><App adminUrl="https://admin.example/" api={api} {...props}/></div></div>);
|
||||
const iframeElement = container.querySelector('iframe[title="comments-frame"]');
|
||||
expect(iframeElement).toBeInTheDocument();
|
||||
const iframeDocument = iframeElement.contentDocument;
|
||||
|
||||
return {container, api, iframeDocument};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
window.scrollTo = vi.fn();
|
||||
Range.prototype.getClientRects = function getClientRects() {
|
||||
return [
|
||||
{
|
||||
bottom: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Comments', () => {
|
||||
it('renders comments', async () => {
|
||||
const {api, iframeDocument} = renderApp();
|
||||
vi.spyOn(api.comments, 'browse').mockImplementation(() => {
|
||||
return {
|
||||
comments: [
|
||||
buildComment({html: '<p>This is a comment body</p>'})
|
||||
],
|
||||
meta: {
|
||||
pagination: {
|
||||
limit: 5,
|
||||
total: 1,
|
||||
next: null,
|
||||
prev: null,
|
||||
page: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const commentBody = await within(iframeDocument).findByText(/This is a comment body/i);
|
||||
expect(commentBody).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows pagination button on top', async () => {
|
||||
const user = userEvent.setup();
|
||||
const limit = 5;
|
||||
|
||||
const {api, iframeDocument} = renderApp();
|
||||
vi.spyOn(api.comments, 'browse').mockImplementation(({page}) => {
|
||||
if (page === 2) {
|
||||
return {
|
||||
comments: new Array(1).fill({}).map(() => buildComment({html: '<p>This is a paginated comment</p>'})),
|
||||
meta: {
|
||||
pagination: {
|
||||
limit,
|
||||
total: limit + 1,
|
||||
next: null,
|
||||
prev: 1,
|
||||
page
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
comments: new Array(limit).fill({}).map(() => buildComment({html: '<p>This is a comment body</p>'})),
|
||||
meta: {
|
||||
pagination: {
|
||||
limit,
|
||||
total: limit + 1,
|
||||
next: 2,
|
||||
prev: null,
|
||||
page
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const comments = await within(iframeDocument).findAllByText(/This is a comment body/i);
|
||||
expect(comments).toHaveLength(limit);
|
||||
const button = await within(iframeDocument).findByText(/Show 1 previous comment/i);
|
||||
|
||||
await user.click(button);
|
||||
await within(iframeDocument).findByText(/This is a paginated comment/i);
|
||||
expect(button).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can handle deleted members', async () => {
|
||||
const limit = 5;
|
||||
|
||||
const {api, iframeDocument} = renderApp();
|
||||
vi.spyOn(api.comments, 'browse').mockImplementation(({page}) => {
|
||||
return {
|
||||
comments: new Array(limit).fill({}).map(() => buildComment({html: '<p>This is a comment body</p>', member: null})),
|
||||
meta: {
|
||||
pagination: {
|
||||
limit,
|
||||
total: limit + 1,
|
||||
next: 2,
|
||||
prev: null,
|
||||
page
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const comments = await within(iframeDocument).findAllByText(/This is a comment body/i);
|
||||
expect(comments).toHaveLength(limit);
|
||||
});
|
||||
|
||||
it('shows a different UI when not logged in', async () => {
|
||||
const limit = 5;
|
||||
|
||||
const {api, iframeDocument} = renderApp();
|
||||
vi.spyOn(api.comments, 'browse').mockImplementation(({page}) => {
|
||||
if (page === 2) {
|
||||
throw new Error('Not requested');
|
||||
}
|
||||
return {
|
||||
comments: new Array(limit).fill({}).map(() => buildComment({html: '<p>This is a comment body</p>'})),
|
||||
meta: {
|
||||
pagination: {
|
||||
limit,
|
||||
total: limit + 1,
|
||||
next: 2,
|
||||
prev: null,
|
||||
page
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const comments = await within(iframeDocument).findAllByText(/This is a comment body/i);
|
||||
expect(comments).toHaveLength(limit);
|
||||
|
||||
// Does not show the reply buttons if not logged in
|
||||
const replyButton = within(iframeDocument).queryByTestId('reply-button');
|
||||
expect(replyButton).toBeNull(); // it doesn't exist
|
||||
|
||||
// Does not show the main form
|
||||
const form = within(iframeDocument).queryByTestId('form');
|
||||
expect(form).toBeNull(); // it doesn't exist
|
||||
|
||||
// todo: Does show the CTA
|
||||
});
|
||||
});
|
||||
|
||||
describe('Likes', () => {
|
||||
it('can like and unlike a comment', async () => {
|
||||
const limit = 5;
|
||||
const member = buildMember();
|
||||
|
||||
const {api, iframeDocument} = renderApp({
|
||||
member
|
||||
});
|
||||
|
||||
vi.spyOn(api.comments, 'browse').mockImplementation(({page}) => {
|
||||
if (page === 2) {
|
||||
throw new Error('Not requested');
|
||||
}
|
||||
return {
|
||||
comments: new Array(1).fill({}).map(() => buildComment({html: '<p>This is a comment body</p>', count: {likes: 5, replies: 0}, liked: false})),
|
||||
meta: {
|
||||
pagination: {
|
||||
limit,
|
||||
total: 1,
|
||||
next: null,
|
||||
prev: null,
|
||||
page
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const likeSpy = vi.spyOn(api.comments, 'like');
|
||||
const unlikeSpy = vi.spyOn(api.comments, 'unlike');
|
||||
|
||||
const comment = await within(iframeDocument).findByTestId('comment-component');
|
||||
|
||||
const likeButton = within(comment).queryByTestId('like-button');
|
||||
expect(likeButton).toBeInTheDocument();
|
||||
|
||||
// Initial likes are 5
|
||||
expect(likeButton.lastChild.textContent).toEqual('5');
|
||||
|
||||
// Check not filled
|
||||
const icon = likeButton.querySelector('svg');
|
||||
|
||||
// SVG className has a different meaning than on normal element (TIL!)
|
||||
// So we have to do this black magic to check the string value
|
||||
expect(icon.className.baseVal).not.toContain('fill');
|
||||
|
||||
await userEvent.click(likeButton);
|
||||
|
||||
expect(likeSpy).toBeCalledTimes(1);
|
||||
|
||||
// Test like icon is filled
|
||||
expect(icon.className.baseVal).toContain('fill');
|
||||
|
||||
// Test count went up with one
|
||||
expect(likeButton.lastChild.textContent).toEqual('6');
|
||||
|
||||
// Can unlike
|
||||
await userEvent.click(likeButton);
|
||||
expect(likeButton.lastChild.textContent).toEqual('5');
|
||||
expect(icon.className.baseVal).not.toContain('fill');
|
||||
|
||||
expect(unlikeSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Replies', () => {
|
||||
// Test is currently hanging for an unknown reason
|
||||
it.skip('can reply to a comment', async () => {
|
||||
const limit = 5;
|
||||
const member = buildMember();
|
||||
|
||||
const {api, iframeDocument} = renderApp({
|
||||
member
|
||||
});
|
||||
|
||||
vi.spyOn(api.comments, 'browse').mockImplementation(({page}) => {
|
||||
if (page === 2) {
|
||||
throw new Error('Not requested');
|
||||
}
|
||||
return {
|
||||
comments: new Array(limit).fill({}).map(() => buildComment({html: '<p>This is a comment body</p>'})),
|
||||
meta: {
|
||||
pagination: {
|
||||
limit,
|
||||
total: limit + 1,
|
||||
next: 2,
|
||||
prev: null,
|
||||
page
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const repliesSpy = vi.spyOn(api.comments, 'replies');
|
||||
|
||||
const comments = await within(iframeDocument).findAllByTestId('comment-component');
|
||||
expect(comments).toHaveLength(limit);
|
||||
|
||||
// Does show the main form
|
||||
const form = within(iframeDocument).queryByTestId('form');
|
||||
expect(form).toBeInTheDocument();
|
||||
|
||||
const replyButton = await within(comments[0]).queryByTestId('reply-button');
|
||||
expect(replyButton).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(replyButton);
|
||||
|
||||
const replyForm = within(comments[0]).queryByTestId('form');
|
||||
expect(replyForm).toBeInTheDocument();
|
||||
|
||||
// todo: Check if the main form has been hidden
|
||||
|
||||
expect(repliesSpy).toBeCalledTimes(1);
|
||||
|
||||
// Enter some text
|
||||
|
||||
const editor = replyForm.querySelector('[contenteditable="true"]');
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(editor, '> This is a quote');
|
||||
fireEvent.keyDown(editor, {key: 'Enter', code: 'Enter', charCode: 13});
|
||||
fireEvent.keyDown(editor, {key: 'Enter', code: 'Enter', charCode: 13});
|
||||
await userEvent.type(editor, 'This is a reply');
|
||||
});
|
||||
|
||||
// Press save
|
||||
const submitButton = within(replyForm).queryByTestId('submit-form-button');
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
// Form should get removed
|
||||
await waitFor(() => {
|
||||
expect(replyForm).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Check if reply is visible
|
||||
const replies = within(comments[0]).queryAllByTestId('comment-component');
|
||||
expect(replies).toHaveLength(1);
|
||||
|
||||
const content = within(replies[0]).queryByTestId('comment-content');
|
||||
expect(content.innerHTML).toEqual('<blockquote><p>This is a quote</p></blockquote><p></p><p>This is a reply</p>');
|
||||
|
||||
// Check if pagination button is NOT visible
|
||||
const replyPagination = within(iframeDocument).queryByTestId('reply-pagination-button');
|
||||
expect(replyPagination).toBeNull(); // it doesn't exist
|
||||
});
|
||||
});
|
107
apps/comments-ui/test/e2e/actions.test.ts
Normal file
107
apps/comments-ui/test/e2e/actions.test.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import {MockedApi, initialize} from '../utils/e2e';
|
||||
import {expect, test} from '@playwright/test';
|
||||
|
||||
test.describe('Actions', async () => {
|
||||
test('Can like and unlike a comment', async ({page}) => {
|
||||
const mockedApi = new MockedApi({});
|
||||
mockedApi.setMember({});
|
||||
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 1</p>'
|
||||
});
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 2</p>',
|
||||
liked: true,
|
||||
count: {
|
||||
likes: 52
|
||||
}
|
||||
});
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 3</p>'
|
||||
});
|
||||
|
||||
const {frame} = await initialize({
|
||||
mockedApi,
|
||||
page,
|
||||
publication: 'Publisher Weekly'
|
||||
});
|
||||
|
||||
// Check like button is not filled yet
|
||||
const comment = frame.getByTestId('comment-component').nth(0);
|
||||
const likeButton = comment.getByTestId('like-button');
|
||||
await expect(likeButton).toHaveCount(1);
|
||||
|
||||
const icon = likeButton.locator('svg');
|
||||
await expect(icon).not.toHaveClass(/fill/);
|
||||
await expect(likeButton).toHaveText('0');
|
||||
|
||||
// Click button
|
||||
await likeButton.click();
|
||||
|
||||
// Check not filled
|
||||
await expect(icon).toHaveClass(/fill/);
|
||||
await expect(likeButton).toHaveText('1');
|
||||
|
||||
// Click button again
|
||||
await likeButton.click();
|
||||
|
||||
await expect(icon).not.toHaveClass(/fill/);
|
||||
await expect(likeButton).toHaveText('0');
|
||||
|
||||
// Check state for already liked comment
|
||||
const secondComment = frame.getByTestId('comment-component').nth(1);
|
||||
const likeButton2 = secondComment.getByTestId('like-button');
|
||||
await expect(likeButton2).toHaveCount(1);
|
||||
const icon2 = likeButton2.locator('svg');
|
||||
await expect(icon2).toHaveClass(/fill/);
|
||||
await expect(likeButton2).toHaveText('52');
|
||||
});
|
||||
|
||||
test('Can reply to a comment', async ({page}) => {
|
||||
const mockedApi = new MockedApi({});
|
||||
mockedApi.setMember({});
|
||||
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 1</p>'
|
||||
});
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 2</p>',
|
||||
liked: true,
|
||||
count: {
|
||||
likes: 52
|
||||
}
|
||||
});
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 3</p>'
|
||||
});
|
||||
|
||||
const {frame} = await initialize({
|
||||
mockedApi,
|
||||
page,
|
||||
publication: 'Publisher Weekly'
|
||||
});
|
||||
|
||||
// Check like button is not filled yet
|
||||
const comment = frame.getByTestId('comment-component').nth(0);
|
||||
const replyButton = comment.getByTestId('reply-button');
|
||||
await expect(replyButton).toHaveCount(1);
|
||||
|
||||
// Click button
|
||||
await replyButton.click();
|
||||
const editor = frame.getByTestId('form-editor');
|
||||
await expect(editor).toBeVisible();
|
||||
|
||||
// Type some text
|
||||
await page.keyboard.type('This is a reply 123');
|
||||
await expect(editor).toHaveText('This is a reply 123');
|
||||
|
||||
// Click reply button
|
||||
const submitButton = comment.getByTestId('submit-form-button');
|
||||
await submitButton.click();
|
||||
|
||||
// Check total amount of comments increased
|
||||
await expect(frame.getByTestId('comment-component')).toHaveCount(4);
|
||||
await expect(frame.getByText('This is a reply 123')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -18,6 +18,14 @@ test.describe('CTA', async () => {
|
|||
await expect(ctaBox).toContainText('Join the discussion');
|
||||
await expect(ctaBox).toContainText('Become a member of Publisher Weekly to start commenting');
|
||||
await expect(ctaBox).toContainText('Sign in');
|
||||
|
||||
// Does not show the reply buttons if not logged in
|
||||
const replyButton = frame.getByTestId('reply-button');
|
||||
await expect(replyButton).toHaveCount(0);
|
||||
|
||||
// Does not show the main form
|
||||
const form = frame.getByTestId('form');
|
||||
await expect(form).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('Shows different CTA if no comments', async ({page}) => {
|
||||
|
@ -57,6 +65,37 @@ test.describe('CTA', async () => {
|
|||
|
||||
// Don't show sign in button
|
||||
await expect(ctaBox).not.toContainText('Sign in');
|
||||
|
||||
// No replies or comments possible
|
||||
const replyButton = frame.getByTestId('reply-button');
|
||||
await expect(replyButton).toHaveCount(0);
|
||||
|
||||
const form = frame.getByTestId('form');
|
||||
await expect(form).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('No CTA when logged in', async ({page}) => {
|
||||
const mockedApi = new MockedApi({});
|
||||
mockedApi.addComments(2);
|
||||
mockedApi.setMember({
|
||||
status: 'free'
|
||||
});
|
||||
|
||||
const {frame} = await initialize({
|
||||
mockedApi,
|
||||
page,
|
||||
publication: 'Publisher Weekly'
|
||||
});
|
||||
|
||||
const ctaBox = await frame.getByTestId('cta-box');
|
||||
await expect(ctaBox).not.toBeVisible();
|
||||
|
||||
// No replies or comments possible
|
||||
const replyButton = frame.getByTestId('reply-button');
|
||||
await expect(replyButton).toHaveCount(2);
|
||||
|
||||
const form = frame.getByTestId('form');
|
||||
await expect(form).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Locator, expect, test} from '@playwright/test';
|
||||
import {MockedApi, getHeight, getModifierKey, initialize, selectText, setClipboard} from '../utils/e2e';
|
||||
import {expect, test} from '@playwright/test';
|
||||
|
||||
test.describe('Editor', async () => {
|
||||
test('Can comment on a post', async ({page}) => {
|
||||
|
|
|
@ -113,5 +113,24 @@ test.describe('Pagination', async () => {
|
|||
await expect(frame.getByText('This is reply 3')).toBeVisible();
|
||||
await expect(frame.getByText('This is reply 4')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Can handle comments with deleted member', async ({page}) => {
|
||||
const mockedApi = new MockedApi({});
|
||||
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 1</p>',
|
||||
member: null
|
||||
});
|
||||
|
||||
const {frame} = await initialize({
|
||||
mockedApi,
|
||||
page,
|
||||
publication: 'Publisher Weekly'
|
||||
});
|
||||
|
||||
await expect(frame.getByTestId('comment-component')).toHaveCount(1);
|
||||
|
||||
await expect(frame.getByText('This is comment 1')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -202,6 +202,40 @@ export class MockedApi {
|
|||
});
|
||||
});
|
||||
|
||||
// LIKE a single comment
|
||||
await page.route(`${path}/members/api/comments/*/like/`, async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const commentId = url.pathname.split('/').reverse()[2];
|
||||
|
||||
const comment = this.comments.find(c => c.id === commentId);
|
||||
if (!comment) {
|
||||
return await route.fulfill({
|
||||
status: 404,
|
||||
body: 'Comment not found'
|
||||
});
|
||||
}
|
||||
|
||||
if (route.request().method() === 'POST') {
|
||||
comment.count.likes += 1;
|
||||
comment.liked = true;
|
||||
}
|
||||
|
||||
if (route.request().method() === 'DELETE') {
|
||||
comment.count.likes -= 1;
|
||||
comment.liked = false;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(this.browseComments({
|
||||
limit: 1,
|
||||
filter: `id:'${commentId}'`,
|
||||
page: 1
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// GET a single comment
|
||||
await page.route(`${path}/members/api/comments/*/`, async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
|
|
|
@ -21,16 +21,17 @@ export function buildComment(override: any = {}) {
|
|||
id: ObjectId().toString(),
|
||||
html: '<p>Empty</p>',
|
||||
replies: [],
|
||||
count: {
|
||||
replies: 0,
|
||||
likes: 0
|
||||
},
|
||||
liked: false,
|
||||
created_at: '2022-08-11T09:26:34.000Z',
|
||||
edited_at: null,
|
||||
member: buildMember(),
|
||||
status: 'published',
|
||||
...override
|
||||
...override,
|
||||
count: {
|
||||
replies: 0,
|
||||
likes: 0,
|
||||
...override.count
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue