0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00
ghost/apps/comments-ui/test/utils/e2e.ts
Ronald Langeveld 04f0b9fc3f
Fixed comment likes being incorrect when logged in as an Admin (#21833)
ref https://linear.app/ghost/issue/PLG-296/

When logged in as an Admin, comments-ui switches comment reads from the Members API over to the Admin API so that hidden comments can be displayed to allow moderation activities. However, the Admin API not using member authentication and CORS preventing the front-end members auth cookie being passed over to the Admin API domain meant that the logged-in member's likes were missing when fetching via the Admin API as there is no available reference to the logged in member.

This change works around the problem by introducing an `impersonate_member_uuid` param to the comments read/browse endpoints of the Admin API. When passed, the provided uuid is used to simulate that member being logged in so that likes are correctly shown.

- Introduced `impersonation_member_id` parameter to resolve issues with admin API not returning correct "liked" status for comments when an admin is logged in.
- Updated API endpoints in `comment-replies.js` and `comments.js` to handle `impersonation_member_id`.
- Adjusted `CommentsController` to validate and process the `impersonation_member_id` parameter before passing it to database queries.
- Enhanced test coverage to ensure proper handling of the new parameter and accurate "liked" status behavior.
2024-12-10 14:44:15 +00:00

261 lines
8.2 KiB
TypeScript

import {E2E_PORT} from '../../playwright.config';
import {Locator, Page} from '@playwright/test';
import {MockedApi} from './MockedApi';
import {expect} from '@playwright/test';
export const MOCKED_SITE_URL = 'https://localhost:1234';
export {MockedApi};
export async function waitEditorFocused(editor: Locator) {
// Wait for focused
const internalEditor = editor.getByTestId('editor');
await expect(internalEditor).toBeFocused();
}
function escapeHtml(unsafe: string) {
return unsafe
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function authFrameMain() {
const endpoints = {
browseComments: ['GET', ['postId'], '/comments/post/$1/'],
getReplies: ['GET', ['commentId'], '/comments/$1/replies/'],
readComment: ['GET', ['commentId'], '/comments/$1/'],
getUser: ['GET', [], '/users/me/'],
hideComment: ['PUT', ['id'], '/comments/$1/', data => ({id: data.id, status: 'hidden'})],
showComment: ['PUT', ['id'], '/comments/$1/', data => ({id: data.id, status: 'published'})]
};
window.addEventListener('message', async function (event) {
let data: any = null; // eslint-disable-line @typescript-eslint/no-explicit-any
try {
data = JSON.parse(event.data) || {};
} catch (err) {
console.error(err); // eslint-disable-line no-console
}
if (!data) {
return;
}
function respond(error, result) {
event.source!.postMessage(JSON.stringify({
uid: data.uid,
error: error?.message,
result
}));
}
if (endpoints[data.action]) {
try {
const [method, routeParams, route, bodyFn] = endpoints[data.action];
const paramData = routeParams.map(param => data[param]);
const path = route.replace(/\$(\d+)/g, (_, index) => paramData[index - 1]);
const url = new URL(`/ghost/api/admin${path}`, MOCKED_SITE_URL);
if (data.params) {
url.search = new URLSearchParams(data.params).toString();
}
let body, headers;
if (method === 'PUT' || method === 'POST') {
body = JSON.stringify(bodyFn(data));
headers = {'Content-Type': 'application/json'};
}
const res = await fetch(url, {method, body, headers});
const json = await res.json();
respond(null, json);
} catch (err) {
console.log('e2e Admin endpoint error:', err); // eslint-disable-line no-console
console.log('error with', data); // eslint-disable-line no-console
respond(err, null);
}
}
});
}
export async function mockAdminAuthFrame({admin, page}) {
await page.route(admin + 'auth-frame/', async (route) => {
await route.fulfill({
status: 200,
body: `<html><head><meta charset="UTF-8" /></head><body><script>${authFrameMain.toString().replaceAll('MOCKED_SITE_URL', `'${MOCKED_SITE_URL}'`)}; authFrameMain();</script></body></html>`
});
});
}
export async function mockAdminAuthFrame204({admin, page}) {
await page.route(admin + 'auth-frame/', async (route) => {
await route.fulfill({
status: 204
});
});
}
export async function initialize({mockedApi, page, bodyStyle, labs = {}, key = '12345678', api = MOCKED_SITE_URL, ...options}: {
mockedApi: MockedApi,
page: Page,
path?: string;
ghostComments?: string,
key?: string,
api?: string,
admin?: string,
colorScheme?: string,
avatarSaturation?: string,
accentColor?: string,
commentsEnabled?: string,
title?: string,
count?: boolean,
publication?: string,
postId?: string,
bodyStyle?: string,
labs?: LabsType
}) {
const sitePath = MOCKED_SITE_URL;
mockedApi.setSettings({
settings: {
labs: {
...labs
}
}
});
await page.route(sitePath, async (route) => {
await route.fulfill({
status: 200,
body: `<html><head><meta charset="UTF-8" /></head><body ${bodyStyle ? 'style="' + escapeHtml(bodyStyle) + '"' : ''}></body></html>`
});
});
const url = `http://localhost:${E2E_PORT}/comments-ui.min.js`;
await page.setViewportSize({width: 1000, height: 1000});
await page.goto(sitePath);
await mockedApi.listen({page, path: sitePath});
if (!options.ghostComments) {
options.ghostComments = MOCKED_SITE_URL;
}
if (!options.postId) {
options.postId = mockedApi.postId;
}
if (!options.key) {
options.key = key;
}
if (!options.api) {
options.api = api;
}
await page.evaluate((data) => {
const scriptTag = document.createElement('script');
scriptTag.src = data.url;
for (const option of Object.keys(data.options)) {
scriptTag.dataset[option] = data.options[option];
}
document.body.appendChild(scriptTag);
}, {url, options});
const commentsFrameSelector = 'iframe[title="comments-frame"]';
await page.waitForSelector('iframe');
// wait for data to be loaded because our tests expect it
const iframeElement = await page.locator(commentsFrameSelector).elementHandle();
if (!iframeElement) {
throw new Error('iframe not found');
}
const iframe = await iframeElement.contentFrame();
if (!iframe) {
throw new Error('iframe contentFrame not found');
}
await iframe.waitForSelector('[data-loaded="true"]');
return {
frame: page.frameLocator(commentsFrameSelector)
};
}
/**
* Select text range by RegExp.
*/
export async function selectText(locator: Locator, pattern: string | RegExp): Promise<void> {
await locator.evaluate(
(element, {pattern: p}) => {
let textNode = element.childNodes[0];
while (textNode.nodeType !== Node.TEXT_NODE && textNode.childNodes.length) {
textNode = textNode.childNodes[0];
}
const match = textNode.textContent?.match(new RegExp(p));
if (match) {
const range = document.createRange();
range.setStart(textNode, match.index!);
range.setEnd(textNode, match.index! + match[0].length);
const selection = document.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
},
{pattern}
);
}
export async function getHeight(locator: Locator) {
return await locator.evaluate((node) => {
return node.clientHeight;
});
}
export async function setClipboard(page, text) {
const modifier = getModifierKey();
await page.setContent(`<div contenteditable>${text}</div>`);
await page.focus('div');
await page.keyboard.press(`${modifier}+KeyA`);
await page.keyboard.press(`${modifier}+KeyC`);
}
export function getModifierKey() {
const os = require('os'); // eslint-disable-line @typescript-eslint/no-var-requires
const platform = os.platform();
if (platform === 'darwin') {
return 'Meta';
} else {
return 'Control';
}
}
export function addMultipleComments(api, numComments) {
for (let i = 1; i <= numComments; i++) {
api.addComment({
html: `<p>This is comment ${i}.</p>`
});
}
}
export async function waitForFrameOpacity(frameLocator, selector, timeout = 2000) {
const start = Date.now();
while (Date.now() - start < timeout) {
// Evaluate the opacity of the element within the frame
const opacity = await frameLocator.locator(selector).evaluate((element) => {
return window.getComputedStyle(element).opacity;
});
// Check if opacity is 1 (100%)
if (opacity === '1') {
return;
}
// Wait a little before retrying
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}
throw new Error(`Element ${selector} did not reach 100% opacity within ${timeout} ms`);
}