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
2 changes: 1 addition & 1 deletion .lintstagedrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function buildScopedEslintCommand(workspace, files) {
.map(shellQuote)
.join(' ');

return `cd ${shellQuote(workspace)} && eslint --cache ${relativeFiles}`;
return `yarn --cwd ${shellQuote(workspace)} eslint --cache ${relativeFiles}`;
}

function buildRootEslintCommand(files) {
Expand Down
60 changes: 60 additions & 0 deletions e2e/helpers/pages/admin/posts/post/post-editor-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ class SettingsMenu extends BasePage {
readonly postUrlInput: Locator;
readonly publishDateInput: Locator;
readonly publishTimeInput: Locator;
readonly deletePostButton: Locator;
readonly deletePostConfirmButton: Locator;

constructor(page: Page) {
super(page);

this.postUrlInput = page.getByRole('textbox', {name: 'Post URL'});
this.publishDateInput = page.getByLabel('Date Picker');
this.publishTimeInput = page.getByLabel('Time Picker');
this.deletePostButton = page.locator('[data-test-button="delete-post"]');
this.deletePostConfirmButton = page.locator('[data-test-button="delete-post-confirm"]');
}

async deletePost(): Promise<void> {
await this.deletePostButton.click();
await this.deletePostConfirmButton.click();
}
}

Expand All @@ -24,6 +33,8 @@ class PublishFlow extends BasePage {
readonly emailRecipientsSetting: Locator;
readonly continueButton: Locator;
readonly confirmButton: Locator;
readonly closeButton: Locator;
readonly completeBookmark: Locator;

constructor(page: Page) {
super(page);
Expand All @@ -34,12 +45,18 @@ class PublishFlow extends BasePage {
this.emailRecipientsSetting = page.locator('[data-test-setting="email-recipients"]');
this.continueButton = page.locator('[data-test-modal="publish-flow"] [data-test-button="continue"]');
this.confirmButton = page.locator('[data-test-modal="publish-flow"] [data-test-button="confirm-publish"]');
this.closeButton = page.locator('[data-test-button="close-publish-flow"]');
this.completeBookmark = page.locator('[data-test-complete-bookmark]');
}

async open(): Promise<void> {
await this.publishButton.click();
}

async close(): Promise<void> {
await this.closeButton.click();
}

async selectPublishType(type: 'publish' | 'publish+send' | 'send'): Promise<void> {
await this.publishTypeButton.click();
await this.page.locator(`[data-test-publish-type="${type}"] + label`).click();
Expand All @@ -50,6 +67,14 @@ class PublishFlow extends BasePage {
await this.confirmButton.click({force: true});
await this.confirmButton.waitFor({state: 'hidden'});
}

async openPublishedPost(): Promise<Page> {
const [frontendPage] = await Promise.all([
this.page.waitForEvent('popup'),
this.completeBookmark.click()
]);
return frontendPage;
}
}

export class PostEditorPage extends AdminPage {
Expand All @@ -59,6 +84,9 @@ export class PostEditorPage extends AdminPage {
readonly previewModal: PostPreviewModal;
readonly settingsToggleButton: Locator;
readonly publishFlow: PublishFlow;
readonly screenTitle: Locator;
readonly lexicalEditor: Locator;
readonly secondaryEditor: Locator;

readonly settingsMenu: SettingsMenu;

Expand All @@ -72,6 +100,9 @@ export class PostEditorPage extends AdminPage {
this.previewModal = new PostPreviewModal(page);
this.settingsToggleButton = page.getByTestId('settings-menu-toggle');
this.publishFlow = new PublishFlow(page);
this.screenTitle = page.locator('[data-test-screen-title]');
this.lexicalEditor = page.locator('[data-kg="editor"]').first();
this.secondaryEditor = page.locator('[data-secondary-instance="true"]');

this.settingsMenu = new SettingsMenu(page);
}
Expand All @@ -81,6 +112,35 @@ export class PostEditorPage extends AdminPage {
await this.titleInput.waitFor({state: 'visible'});
}

async createDraft({title = 'Hello world', body = 'This is my post body.'} = {}): Promise<void> {
const editor = this.page.locator('[data-lexical-editor="true"]').first();

await this.titleInput.click();
await this.titleInput.fill(title);
await editor.waitFor({state: 'visible'});
await this.page.keyboard.press('Enter');

await this.page.waitForFunction(() => {
const element = document.querySelector('[data-lexical-editor="true"]');
if (!element) {
return false;
}

const activeElement = document.activeElement;

return Boolean(
activeElement &&
(activeElement === element || element.contains(activeElement))
);
});

await this.page.keyboard.type(body);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async waitForSaved(): Promise<void> {
await this.postStatus.filter({hasText: /Saved/}).waitFor({timeout: 30000});
}

get previewModalDesktopFrame(): DesktopPreviewFrame {
return this.previewModal.desktopPreview;
}
Expand Down
4 changes: 4 additions & 0 deletions e2e/helpers/pages/public/post-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {PublicPage} from './public-page';
export class PostPage extends PublicPage {
readonly postTitle: Locator;
readonly postContent: Locator;
readonly articleTitle: Locator;
readonly articleBody: Locator;
readonly commentsSection: CommentsSection;
readonly transistorCard: Locator;
readonly transistorIframe: Locator;
Expand All @@ -14,6 +16,8 @@ export class PostPage extends PublicPage {
super(page);
this.postTitle = page.locator('article h1').first();
this.postContent = page.locator('article.gh-article');
this.articleTitle = page.locator('.gh-article-title');
this.articleBody = page.locator('.gh-content.gh-canvas > p');
this.commentsSection = new CommentsSection(page);
this.transistorCard = page.locator('.kg-transistor-card');
this.transistorIframe = page.locator('iframe[data-kg-transistor-embed]');
Expand Down
27 changes: 27 additions & 0 deletions e2e/tests/admin/posts/lexical-editor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {PostEditorPage, PostsPage} from '@/admin-pages';
import {expect, test} from '@/helpers/playwright';

test.describe('Ghost Admin - Lexical Editor', () => {
test('renders primary lexical editor', async ({page}) => {
const postsPage = new PostsPage(page);
await postsPage.goto();
await postsPage.newPostButton.click();

const editor = new PostEditorPage(page);
await editor.createDraft({title: 'Lexical editor test', body: 'This is my post body.'});

await expect(editor.lexicalEditor).toBeVisible();
});

test('renders secondary hidden lexical editor', async ({page}) => {
const postsPage = new PostsPage(page);
await postsPage.goto();
await postsPage.newPostButton.click();

const editor = new PostEditorPage(page);
await editor.createDraft({title: 'Secondary lexical editor test', body: 'This is my post body.'});

await expect(editor.secondaryEditor).toHaveCount(1);
await expect(editor.secondaryEditor).toBeHidden();
});
});
122 changes: 122 additions & 0 deletions e2e/tests/admin/posts/publishing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {APIRequestContext} from '@playwright/test';
import {PostEditorPage, PostsPage} from '@/admin-pages';
import {PostPage} from '@/helpers/pages';
import {createMemberFactory, generateSlug} from '@/data-factory';
import {expect, test} from '@/helpers/playwright';

async function getNewsletters(request: APIRequestContext): Promise<string[]> {
const response = await request.get('/ghost/api/admin/newsletters/?status=active&limit=all');
const data = await response.json();
return data.newsletters.map((n: {id: string}) => n.id);
}

test.describe('Ghost Admin - Publishing', () => {
test.use({mailgunEnabled: true});

test('publish only - post is visible on frontend', async ({page}) => {
const postData = {title: 'Publish post only', body: 'This is my post body.'};

const postsPage = new PostsPage(page);
await postsPage.goto();
await postsPage.newPostButton.click();

const editor = new PostEditorPage(page);
await editor.createDraft(postData);

await editor.publishFlow.open();
await editor.publishFlow.confirm();
await editor.publishFlow.close();

const postPage = new PostPage(page);
await postPage.goto(`/${generateSlug(postData.title)}/`);
await expect(postPage.articleTitle).toHaveText(postData.title);
await expect(postPage.articleBody).toHaveText(postData.body);
});

test('publish and email - post is visible on frontend', async ({page}) => {
const postData = {title: 'Publish and email post', body: 'This is my post body.'};

const memberFactory = createMemberFactory(page.request);
const newsletters = await getNewsletters(page.request);
await memberFactory.create({
email: 'publish-email-test@example.com',
name: 'Publishing member',
newsletters
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const postsPage = new PostsPage(page);
await postsPage.goto();
await postsPage.newPostButton.click();

const editor = new PostEditorPage(page);
await editor.createDraft(postData);

await editor.publishFlow.open();
await editor.publishFlow.selectPublishType('publish+send');
await editor.publishFlow.confirm();

const postPage = new PostPage(page);
await postPage.goto(`/${generateSlug(postData.title)}/`);
await expect(postPage.articleTitle).toHaveText(postData.title);
await expect(postPage.articleBody).toHaveText(postData.body);
});

test('email only - post is not visible on frontend', async ({page}) => {
const postData = {title: 'Email only post', body: 'This is my post body.'};

const memberFactory = createMemberFactory(page.request);
const newsletters = await getNewsletters(page.request);
await memberFactory.create({
email: 'email-only-test@example.com',
name: 'Publishing member',
newsletters
});

const postsPage = new PostsPage(page);
await postsPage.goto();
await postsPage.newPostButton.click();

const editor = new PostEditorPage(page);
await editor.createDraft(postData);

await editor.publishFlow.open();
await editor.publishFlow.selectPublishType('send');
await editor.publishFlow.confirm();

const slug = generateSlug(postData.title);
const response = await page.goto(`/${slug}/`);
expect(response?.status()).toBe(404);
});
});

test.describe('Ghost Admin - Deleting Posts', () => {
test('delete a saved post - redirects to posts list', async ({page}) => {
const postsPage = new PostsPage(page);
await postsPage.goto();
await postsPage.newPostButton.click();

const editor = new PostEditorPage(page);
await editor.titleInput.fill('Delete a post test');
await editor.titleInput.press('Enter');
await expect(editor.postStatus).toContainText('Draft - Saved');

await editor.settingsToggleButton.click();
await editor.settingsMenu.deletePost();

await expect(editor.screenTitle).toContainText('Posts');
});

test('delete a post with unsaved changes - redirects to posts list', async ({page}) => {
const postsPage = new PostsPage(page);
await postsPage.goto();
await postsPage.newPostButton.click();

const editor = new PostEditorPage(page);
await editor.createDraft({title: 'Delete a post test', body: 'This is the content'});

await editor.settingsToggleButton.click();
await editor.settingsMenu.deletePost();

await expect(editor.screenTitle).toContainText('Posts');
});
});
Loading
Loading