mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
ref https://linear.app/ghost/issue/PLG-230 closes https://linear.app/ghost/issue/PLG-256 Adding an in-reply-to reference link/snippet to reply forms was proving difficult with the previous setup due the amount of data that needed to be passed up and down a deeply nested component tree. This refactor lays the groundwork for making that easier and aims to make form autoclose behaviour more centralised by keeping the open form state in app context and the opening/closing of forms in actions so there's less need for messy local state and to drill functions down the component tree. - replaces `openFormCount` context state with an `openCommentForms` array - keeping detailed open form references in the application state means the display of forms is centrally managed rather than managed via local state inside components - it simplifies some of the problems faced with the `<PublishedComment>` component that previously managed form display. That component is re-used for both the top-level comment as well as replies even though replying on a reply puts the top-level comment into reply mode meaning we had a mess of local state and passed-through functions to work around the component having varying behaviour depending on nesting level - `openFormCount` is still available on the application state via `useMemo` on the provider meaning the implementation of `openCommentForms` is hidden from app code that just needs to know if forms are open - removes `<AutocloseForm>` as the autoclose behaviour is now controlled via the `openCommentForm` action - updated `<Form>` so it manages the "has unsaved changes" properties on `openFormComments` ready for use by `openCommentForm`'s autoclosing behaviour
436 lines
14 KiB
TypeScript
436 lines
14 KiB
TypeScript
import {MockedApi, initialize, waitEditorFocused} from '../utils/e2e';
|
|
import {expect, test} from '@playwright/test';
|
|
|
|
test.describe('Actions', async () => {
|
|
let mockedApi: MockedApi;
|
|
|
|
test.beforeEach(async () => {
|
|
mockedApi = new MockedApi({});
|
|
mockedApi.setMember({});
|
|
});
|
|
|
|
test('Can like and unlike a comment', async ({page}) => {
|
|
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}) => {
|
|
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();
|
|
// Wait for focused
|
|
await waitEditorFocused(editor);
|
|
|
|
// 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);
|
|
});
|
|
|
|
test('Reply-to-reply action not shown without labs flag', async ({
|
|
page
|
|
}) => {
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 1</p>',
|
|
replies: [
|
|
mockedApi.buildReply({
|
|
html: '<p>This is a reply to 1</p>'
|
|
})
|
|
]
|
|
});
|
|
|
|
const {frame} = await initialize({
|
|
mockedApi,
|
|
page,
|
|
publication: 'Publisher Weekly'
|
|
});
|
|
|
|
const parentComment = frame.getByTestId('comment-component').nth(0);
|
|
const replyComment = parentComment.getByTestId('comment-component').nth(0);
|
|
|
|
expect(replyComment.getByTestId('reply-button')).not.toBeVisible();
|
|
});
|
|
|
|
test('Can reply to a reply', async ({page}) => {
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 1</p>',
|
|
replies: [
|
|
mockedApi.buildReply({
|
|
html: '<p>This is a reply to 1</p>'
|
|
})
|
|
]
|
|
});
|
|
|
|
const {frame} = await initialize({
|
|
mockedApi,
|
|
page,
|
|
publication: 'Publisher Weekly',
|
|
labs: {
|
|
commentImprovements: true
|
|
}
|
|
});
|
|
|
|
const parentComment = frame.getByTestId('comment-component').nth(0);
|
|
const replyComment = parentComment.getByTestId('comment-component').nth(0);
|
|
|
|
const replyReplyButton = replyComment.getByTestId('reply-button');
|
|
await replyReplyButton.click();
|
|
|
|
const editor = frame.getByTestId('form-editor').nth(1);
|
|
await expect(editor).toBeVisible();
|
|
await waitEditorFocused(editor);
|
|
|
|
await page.keyboard.type('This is a reply to a reply');
|
|
|
|
const submitButton = parentComment.getByTestId('submit-form-button');
|
|
await submitButton.click();
|
|
|
|
await expect(frame.getByTestId('comment-component')).toHaveCount(3);
|
|
await expect(frame.getByText('This is a reply to a reply')).toHaveCount(1);
|
|
});
|
|
|
|
test('Can add expertise', async ({page}) => {
|
|
mockedApi.setMember({name: 'John Doe', expertise: null});
|
|
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 1</p>'
|
|
});
|
|
|
|
const {frame} = await initialize({
|
|
mockedApi,
|
|
page,
|
|
publication: 'Publisher Weekly'
|
|
});
|
|
|
|
const editor = frame.getByTestId('form-editor');
|
|
await editor.click({force: true});
|
|
await waitEditorFocused(editor);
|
|
|
|
const expertiseButton = frame.getByTestId('expertise-button');
|
|
await expect(expertiseButton).toBeVisible();
|
|
await expect(expertiseButton).toHaveText('·Add your expertise');
|
|
await expertiseButton.click();
|
|
|
|
const detailsFrame = page.frameLocator('iframe[title="addDetailsPopup"]');
|
|
const profileModal = detailsFrame.getByTestId('profile-modal');
|
|
await expect(profileModal).toBeVisible();
|
|
|
|
await expect(detailsFrame.getByTestId('name-input')).toHaveValue(
|
|
'John Doe'
|
|
);
|
|
await expect(detailsFrame.getByTestId('expertise-input')).toHaveValue('');
|
|
|
|
await detailsFrame.getByTestId('name-input').fill('Testy McTest');
|
|
await detailsFrame
|
|
.getByTestId('expertise-input')
|
|
.fill('Software development');
|
|
|
|
await detailsFrame.getByTestId('save-button').click();
|
|
|
|
await expect(profileModal).not.toBeVisible();
|
|
|
|
// playwright can lose focus on the editor which hides the member details,
|
|
// re-clicking here brings the member details back into view
|
|
await editor.click({force: true});
|
|
await waitEditorFocused(editor);
|
|
|
|
await expect(frame.getByTestId('member-name')).toHaveText('Testy McTest');
|
|
await expect(frame.getByTestId('expertise-button')).toHaveText(
|
|
'·Software development'
|
|
);
|
|
});
|
|
|
|
test.describe('Sorting - flag needs to be enabled', () => {
|
|
test('Renders Sorting Form dropdown', async ({page}) => {
|
|
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 4</p>'
|
|
});
|
|
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 5</p>'
|
|
});
|
|
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 6</p>'
|
|
});
|
|
|
|
const {frame} = await initialize({
|
|
mockedApi,
|
|
page,
|
|
publication: 'Publisher Weekly',
|
|
labs: {
|
|
commentImprovements: true
|
|
}
|
|
});
|
|
|
|
const sortingForm = frame.getByTestId('comments-sorting-form');
|
|
|
|
await expect(sortingForm).toBeVisible();
|
|
});
|
|
|
|
test('Default sorting is by Best', async ({page}) => {
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 1</p>',
|
|
count: {
|
|
likes: 5
|
|
},
|
|
createdAt: '2021-01-01T00:00:00Z'
|
|
});
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 2</p>',
|
|
count: {
|
|
likes: 10
|
|
},
|
|
created_at: new Date('2023-01-01T00:00:00Z')
|
|
});
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 3</p>',
|
|
count: {
|
|
likes: 15
|
|
},
|
|
created_at: new Date('2022-02-01T00:00:00Z')
|
|
});
|
|
|
|
const {frame} = await initialize({
|
|
mockedApi,
|
|
page,
|
|
publication: 'Publisher Weekly',
|
|
labs: {
|
|
commentImprovements: true
|
|
}
|
|
});
|
|
|
|
const sortingForm = frame.getByTestId('comments-sorting-form');
|
|
|
|
// Check default sorting is by Best
|
|
|
|
await expect(sortingForm).toHaveText('Best');
|
|
|
|
const comments = await frame.getByTestId('comment-component');
|
|
|
|
await expect(comments.nth(0)).toContainText('This is comment 3');
|
|
});
|
|
test('Renders Sorting Form dropdown, with Best, Newest Oldest', async ({
|
|
page
|
|
}) => {
|
|
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 4</p>'
|
|
});
|
|
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 5</p>'
|
|
});
|
|
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 6</p>'
|
|
});
|
|
|
|
const {frame} = await initialize({
|
|
mockedApi,
|
|
page,
|
|
publication: 'Publisher Weekly',
|
|
labs: {
|
|
commentImprovements: true
|
|
}
|
|
});
|
|
|
|
const sortingForm = frame.getByTestId('comments-sorting-form');
|
|
|
|
await expect(sortingForm).toBeVisible();
|
|
|
|
await sortingForm.click();
|
|
|
|
const sortingDropdown = frame.getByTestId(
|
|
'comments-sorting-form-dropdown'
|
|
);
|
|
await expect(sortingDropdown).toBeVisible();
|
|
|
|
// check if inner options are visible
|
|
|
|
const bestOption = sortingDropdown.getByText('Best');
|
|
const newestOption = sortingDropdown.getByText('Newest');
|
|
const oldestOption = sortingDropdown.getByText('Oldest');
|
|
await expect(bestOption).toBeVisible();
|
|
await expect(newestOption).toBeVisible();
|
|
await expect(oldestOption).toBeVisible();
|
|
});
|
|
|
|
test('Sorts by Newest', async ({page}) => {
|
|
mockedApi.addComment({
|
|
html: '<p>This is the oldest</p>',
|
|
created_at: new Date('2024-02-01T00:00:00Z')
|
|
});
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 2</p>',
|
|
created_at: new Date('2024-03-02T00:00:00Z')
|
|
});
|
|
mockedApi.addComment({
|
|
html: '<p>This is the newest comment</p>',
|
|
created_at: new Date('2024-04-03T00:00:00Z')
|
|
});
|
|
|
|
const {frame} = await initialize({
|
|
mockedApi,
|
|
page,
|
|
publication: 'Publisher Weekly',
|
|
labs: {
|
|
commentImprovements: true
|
|
}
|
|
});
|
|
|
|
const sortingForm = await frame.getByTestId('comments-sorting-form');
|
|
|
|
await sortingForm.click();
|
|
|
|
const sortingDropdown = await frame.getByTestId(
|
|
'comments-sorting-form-dropdown'
|
|
);
|
|
|
|
const newestOption = await sortingDropdown.getByText('Newest');
|
|
await newestOption.click();
|
|
|
|
const comments = await frame.getByTestId('comment-component');
|
|
|
|
await expect(comments.nth(0)).toContainText('This is the newest comment');
|
|
});
|
|
|
|
test('Sorts by oldest', async ({page}) => {
|
|
mockedApi.addComment({
|
|
html: '<p>This is the oldest</p>',
|
|
created_at: new Date('2024-02-01T00:00:00Z')
|
|
});
|
|
mockedApi.addComment({
|
|
html: '<p>This is comment 2</p>',
|
|
created_at: new Date('2024-03-02T00:00:00Z')
|
|
});
|
|
mockedApi.addComment({
|
|
html: '<p>This is the newest comment</p>',
|
|
created_at: new Date('2024-04-03T00:00:00Z')
|
|
});
|
|
|
|
const {frame} = await initialize({
|
|
mockedApi,
|
|
page,
|
|
publication: 'Publisher Weekly',
|
|
labs: {
|
|
commentImprovements: true
|
|
}
|
|
});
|
|
|
|
const sortingForm = await frame.getByTestId('comments-sorting-form');
|
|
|
|
await sortingForm.click();
|
|
|
|
const sortingDropdown = await frame.getByTestId(
|
|
'comments-sorting-form-dropdown'
|
|
);
|
|
|
|
const newestOption = await sortingDropdown.getByText('Oldest');
|
|
await newestOption.click();
|
|
|
|
const comments = await frame.getByTestId('comment-component');
|
|
|
|
await expect(comments.nth(0)).toContainText('This is the oldest');
|
|
});
|
|
});
|
|
});
|