Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/comments-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,11 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
} catch (e) {
// Loading of admin failed. Could be not signed in, or a different error (not important)
// eslint-disable-next-line no-console
console.warn(`[Comments] Failed to fetch current admin user:`, e);
console.warn(`[Comments] Failed to fetch admin endpoint:`, e);
}

setState({
adminApi: adminApi,
adminApi,
admin
});
} catch (e) {
Expand Down
2 changes: 1 addition & 1 deletion apps/comments-ui/src/components/content/Loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {ReactComponent as SpinnerIcon} from '../../images/icons/spinner.svg';
function Loading() {
return (
<div className="flex h-32 w-full items-center justify-center" data-testid="loading">
<SpinnerIcon className="mb-6 h-12 w-12 fill-white/90 dark:fill-white/60" />
<SpinnerIcon className="mb-6 h-12 w-12 fill-neutral-900/90 dark:fill-white/60" />
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ const AdminContextMenu: React.FC<Props> = ({comment, close}) => {
<div className="flex w-full flex-col gap-0.5">
{
isHidden ?
<button className="w-full rounded px-2.5 py-1.5 text-left text-[14px] transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-700" type="button" onClick={showComment}>
<button className="w-full rounded px-2.5 py-1.5 text-left text-[14px] transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-700" data-testid="show-button" type="button" onClick={showComment}>
<span className="hidden sm:inline">{t('Show comment')}</span><span className="sm:hidden">{t('Show')}</span>
</button>
:
<button className="w-full rounded px-2.5 py-1.5 text-left text-[14px] text-red-600 transition-colors hover:bg-neutral-100 dark:text-red-500 dark:hover:bg-neutral-700" type="button" onClick={hideComment}>
<button className="w-full rounded px-2.5 py-1.5 text-left text-[14px] text-red-600 transition-colors hover:bg-neutral-100 dark:text-red-500 dark:hover:bg-neutral-700" data-testid="hide-button" type="button" onClick={hideComment}>
<span className="hidden sm:inline">{t('Hide comment')}</span><span className="sm:hidden">{t('Hide')}</span>
</button>
}
Expand Down
3 changes: 2 additions & 1 deletion apps/comments-ui/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}: {site
return fetch(url, options);
}

// To fix pagination when we create new comments (or people post comments after you loaded the page, we need to only load comments creatd AFTER the page load)
// To fix pagination when we create new comments (or people post comments
// after you loaded the page), we need to only load comments created AFTER the page load
let firstCommentCreatedAt: null | string = null;

const api = {
Expand Down
173 changes: 173 additions & 0 deletions apps/comments-ui/test/e2e/admin-moderation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import sinon from 'sinon';
import {MOCKED_SITE_URL, MockedApi, initialize, mockAdminAuthFrame, mockAdminAuthFrame204} from '../utils/e2e';
import {expect, test} from '@playwright/test';

const admin = MOCKED_SITE_URL + '/ghost/';

test.describe('Admin moderation', async () => {
let mockedApi: MockedApi;

test.beforeEach(async ({}) => {
mockedApi = new MockedApi({});
});

type InitializeTestOptions = {
isAdmin?: boolean;
labs?: boolean;
member?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
};
async function initializeTest(page, options: InitializeTestOptions = {}) {
options = {isAdmin: true, labs: false, member: {id: '1'}, ...options};

if (options.isAdmin) {
await mockAdminAuthFrame({page, admin});
} else {
await mockAdminAuthFrame204({page, admin});
}

mockedApi.setMember(options.member);

if (options.labs) {
mockedApi.setLabs({commentImprovements: true});
}

return await initialize({
mockedApi,
page,
publication: 'Publisher Weekly',
admin,
labs: {
commentImprovements: options.labs
}
});
}

test('skips rendering the auth frame with no comments', async ({page}) => {
await initializeTest(page);

const iframeElement = page.locator('iframe[data-frame="admin-auth"]');
await expect(iframeElement).toHaveCount(0);
});

test('renders the auth frame when there are comments', async ({page}) => {
mockedApi.addComment({html: '<p>This is comment 1</p>'});
await initializeTest(page);

const iframeElement = page.locator('iframe[data-frame="admin-auth"]');
await expect(iframeElement).toHaveCount(1);
});

test('has no admin options when not signed in to Ghost admin or as member', async ({page}) => {
mockedApi.addComment({html: '<p>This is comment 1</p>'});

const {frame} = await initializeTest(page, {isAdmin: false, member: null});
await expect(frame.getByTestId('more-button')).toHaveCount(0);
});

test('has no admin options when not signed in to Ghost admin but signed in as member', async ({page}) => {
mockedApi.addComment({html: '<p>This is comment 1</p>'});

const {frame} = await initializeTest(page, {isAdmin: false, member: {id: '2'}});
// more button shows because it has a report button
await expect(frame.getByTestId('more-button')).toHaveCount(1);

await frame.getByTestId('more-button').nth(0).click();
await expect(frame.getByTestId('hide-button')).not.toBeVisible();
});

test('has admin options when signed in to Ghost admin but not signed in as member', async ({page}) => {
mockedApi.addComment({html: `<p>This is comment 1</p>`});
const {frame} = await initializeTest(page, {member: null});

const moreButtons = frame.getByTestId('more-button');
await expect(moreButtons).toHaveCount(1);

// Admin buttons should be visible
await moreButtons.nth(0).click();
await expect(frame.getByTestId('hide-button')).toBeVisible();
});

test('has admin options when signed in to Ghost admin and as a member', async ({page}) => {
mockedApi.addComment({html: `<p>This is comment 1</p>`});
const {frame} = await initializeTest(page);

const moreButtons = frame.getByTestId('more-button');
await expect(moreButtons).toHaveCount(1);

// Admin buttons should be visible
await moreButtons.nth(0).click();
await expect(frame.getByTestId('hide-button')).toBeVisible();
});

test('can hide and show comments', async ({page}) => {
mockedApi.addComment({html: '<p>This is comment 1</p>'});
mockedApi.addComment({html: '<p>This is comment 2</p>'});

const {frame} = await initializeTest(page);

// Click the hide button for 2nd comment
const moreButtons = frame.getByTestId('more-button');
await moreButtons.nth(1).click();
await moreButtons.nth(1).getByTestId('hide-button').click();

// comment becomes hidden
const comments = frame.getByTestId('comment-component');
const secondComment = comments.nth(1);
await expect(secondComment).toContainText('This comment has been hidden.');
await expect(secondComment).not.toContainText('This is comment 2');

// can show it again
await moreButtons.nth(1).click();
await moreButtons.nth(1).getByTestId('show-button').click();
await expect(secondComment).toContainText('This is comment 2');
});

test.describe('commentImprovements', function () {
test('hidden comments are not displayed for non-admins', async ({page}) => {
mockedApi.addComment({html: '<p>This is comment 1</p>'});
mockedApi.addComment({html: '<p>This is comment 2</p>', status: 'hidden'});

const adminBrowseSpy = sinon.spy(mockedApi.adminRequestHandlers, 'browseComments');

const {frame} = await initializeTest(page, {isAdmin: false, labs: true});
const comments = await frame.getByTestId('comment-component');
await expect(comments).toHaveCount(1);

expect(adminBrowseSpy.called).toBe(false);
});

test('hidden comments are displayed for admins', async ({page}) => {
mockedApi.addComment({html: '<p>This is comment 1</p>'});
mockedApi.addComment({html: '<p>This is comment 2</p>', status: 'hidden'});

const adminBrowseSpy = sinon.spy(mockedApi.adminRequestHandlers, 'browseComments');

const {frame} = await initializeTest(page, {labs: true});
const comments = await frame.getByTestId('comment-component');
await expect(comments).toHaveCount(2);
await expect(comments.nth(1)).toContainText('Hidden for members');

expect(adminBrowseSpy.called).toBe(true);
});

test('can hide and show comments', async ({page}) => {
[1,2].forEach(i => mockedApi.addComment({html: `<p>This is comment ${i}</p>`}));

const {frame} = await initializeTest(page, {labs: true});
const comments = await frame.getByTestId('comment-component');

// Hide the 2nd comment
const moreButtons = frame.getByTestId('more-button');
await moreButtons.nth(1).click();
await moreButtons.nth(1).getByText('Hide comment').click();

const secondComment = comments.nth(1);
await expect(secondComment).toContainText('Hidden for members');

// Check can show it again
await moreButtons.nth(1).click();
await moreButtons.nth(1).getByText('Show comment').click();
await expect(secondComment).toContainText('This is comment 2');
});
});
});
Loading