From b6a7cf180eeaf8bd2ad27124bb10520b2e003496 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 27 Mar 2026 10:05:05 -0500 Subject: [PATCH 1/9] Migrated core publishing e2e tests from browser suite to e2e suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved 7 tests (publish only, publish+email, email only, delete saved post, delete with unsaved changes, lexical editor, secondary lexical editor) from ghost/core/test/e2e-browser/admin/publishing.spec.js to the e2e suite. This is the first slice of an incremental migration — the simplest, most deterministic tests that don't involve scheduling or access control. --- .../admin/posts/post/post-editor-page.ts | 40 ++++++ e2e/helpers/pages/public/post-page.ts | 4 + e2e/tests/admin/posts/lexical-editor.test.ts | 27 ++++ e2e/tests/admin/posts/publishing.test.ts | 124 ++++++++++++++++++ .../test/e2e-browser/admin/publishing.spec.js | 108 +-------------- 5 files changed, 198 insertions(+), 105 deletions(-) create mode 100644 e2e/tests/admin/posts/lexical-editor.test.ts create mode 100644 e2e/tests/admin/posts/publishing.test.ts diff --git a/e2e/helpers/pages/admin/posts/post/post-editor-page.ts b/e2e/helpers/pages/admin/posts/post/post-editor-page.ts index d62b1ad406a..91103e0c2d3 100644 --- a/e2e/helpers/pages/admin/posts/post/post-editor-page.ts +++ b/e2e/helpers/pages/admin/posts/post/post-editor-page.ts @@ -7,6 +7,8 @@ class SettingsMenu extends BasePage { readonly postUrlInput: Locator; readonly publishDateInput: Locator; readonly publishTimeInput: Locator; + readonly deletePostButton: Locator; + readonly deletePostConfirmButton: Locator; constructor(page: Page) { super(page); @@ -14,6 +16,13 @@ class SettingsMenu extends BasePage { 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 { + await this.deletePostButton.click(); + await this.deletePostConfirmButton.click(); } } @@ -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); @@ -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 { await this.publishButton.click(); } + async close(): Promise { + await this.closeButton.click(); + } + async selectPublishType(type: 'publish' | 'publish+send' | 'send'): Promise { await this.publishTypeButton.click(); await this.page.locator(`[data-test-publish-type="${type}"] + label`).click(); @@ -50,6 +67,14 @@ class PublishFlow extends BasePage { await this.confirmButton.click({force: true}); await this.confirmButton.waitFor({state: 'hidden'}); } + + async openPublishedPost(): Promise { + const [frontendPage] = await Promise.all([ + this.page.waitForEvent('popup'), + this.completeBookmark.click() + ]); + return frontendPage; + } } export class PostEditorPage extends AdminPage { @@ -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; @@ -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); } @@ -81,6 +112,15 @@ export class PostEditorPage extends AdminPage { await this.titleInput.waitFor({state: 'visible'}); } + async createDraft({title = 'Hello world', body = 'This is my post body.'} = {}): Promise { + await this.titleInput.click(); + await this.titleInput.fill(title); + await this.page.locator('[data-lexical-editor="true"]').first().waitFor({state: 'visible'}); + await this.page.keyboard.press('Enter'); + await this.page.waitForTimeout(100); + await this.page.keyboard.type(body); + } + get previewModalDesktopFrame(): DesktopPreviewFrame { return this.previewModal.desktopPreview; } diff --git a/e2e/helpers/pages/public/post-page.ts b/e2e/helpers/pages/public/post-page.ts index 50023792364..1003a26020a 100644 --- a/e2e/helpers/pages/public/post-page.ts +++ b/e2e/helpers/pages/public/post-page.ts @@ -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; @@ -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]'); diff --git a/e2e/tests/admin/posts/lexical-editor.test.ts b/e2e/tests/admin/posts/lexical-editor.test.ts new file mode 100644 index 00000000000..88fa51765ae --- /dev/null +++ b/e2e/tests/admin/posts/lexical-editor.test.ts @@ -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(); + }); +}); diff --git a/e2e/tests/admin/posts/publishing.test.ts b/e2e/tests/admin/posts/publishing.test.ts new file mode 100644 index 00000000000..e6143f9818d --- /dev/null +++ b/e2e/tests/admin/posts/publishing.test.ts @@ -0,0 +1,124 @@ +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<{id: 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}) => ({id: n.id})); +} + +test.describe('Ghost Admin - Publishing', () => { + 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 expect(editor.postStatus).toContainText('Draft - Saved'); + + await editor.publishFlow.open(); + await editor.publishFlow.confirm(); + await editor.publishFlow.close(); + + await expect(editor.postStatus).toContainText('Published'); + + 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: newsletters as never + }); + + const postsPage = new PostsPage(page); + await postsPage.goto(); + await postsPage.newPostButton.click(); + + const editor = new PostEditorPage(page); + await editor.createDraft(postData); + await expect(editor.postStatus).toContainText('Draft - Saved'); + + 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: newsletters as never + }); + + const postsPage = new PostsPage(page); + await postsPage.goto(); + await postsPage.newPostButton.click(); + + const editor = new PostEditorPage(page); + await editor.createDraft(postData); + await expect(editor.postStatus).toContainText('Draft - Saved'); + + 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.createDraft({title: 'Delete a post test', body: 'This is the content'}); + 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'); + }); +}); diff --git a/ghost/core/test/e2e-browser/admin/publishing.spec.js b/ghost/core/test/e2e-browser/admin/publishing.spec.js index 3d55fe141c3..b498ef96405 100644 --- a/ghost/core/test/e2e-browser/admin/publishing.spec.js +++ b/ghost/core/test/e2e-browser/admin/publishing.spec.js @@ -201,54 +201,7 @@ const openPublishedPostBookmark = async (page) => { }; test.describe('Publishing', () => { - test.describe('Publish post', () => { - // Post should be available on web and sent as a newsletter - test('Publish and Email', async ({sharedPage}) => { - const postData = { - title: 'Publish and email post', - body: 'This is my post body.' - }; - - // Create a member to send and email to - await createMember(sharedPage, {email: 'test+recipient1@example.com', name: 'Publishing member'}); - - await sharedPage.goto('/ghost'); - await createPostDraft(sharedPage, postData); - await publishPost(sharedPage, {type: 'publish+send'}); - await checkPostPublished(sharedPage, postData); - }); - - // Post should only be available on web - test('Publish only', async ({sharedPage}) => { - const postData = { - title: 'Publish post only', - body: 'This is my post body.' - }; - - await sharedPage.goto('/ghost'); - await createPostDraft(sharedPage, postData); - await publishPost(sharedPage); - await closePublishFlow(sharedPage); - - await checkPostStatus(sharedPage, 'Published'); - await checkPostPublished(sharedPage, postData); - }); - - // Post should be available on web and sent as a newsletter - test('Email only', async ({sharedPage}) => { - const postData = { - title: 'Email only post', - body: 'This is my post body.' - }; - - await createMember(sharedPage, {email: 'test+recipient2@example.com', name: 'Publishing member'}); - - await sharedPage.goto('/ghost'); - await createPostDraft(sharedPage, postData); - await publishPost(sharedPage, {type: 'send'}); - await checkPostNotPublished(sharedPage, postData); - }); - }); + // Publish post tests moved to e2e/tests/admin/posts/publishing.test.ts test.describe('Publish page', () => { // A page can be published and become visible on web @@ -295,28 +248,7 @@ test.describe('Publishing', () => { }); }); - test.describe('Lexical Rendering', () => { - test.describe.configure({retries: 1}); - - test('Renders Lexical editor', async ({sharedPage: adminPage}) => { - await adminPage.goto('/ghost'); - - await createPostDraft(adminPage, {title: 'Lexical editor test', body: 'This is my post body.'}); - - // Check if the lexical editor is present - expect(await adminPage.locator('[data-kg="editor"]').first()).toBeVisible(); - }); - - test('Renders secondary hidden lexical editor', async ({sharedPage: adminPage}) => { - await adminPage.goto('/ghost'); - await createPostDraft(adminPage, {title: 'Secondary lexical editor test', body: 'This is my post body.'}); - const secondaryLexicalEditor = adminPage.locator('[data-secondary-instance="true"]'); - // Check if the secondary lexical editor exists - await expect(secondaryLexicalEditor).toHaveCount(1); - // Check if it is hidden - await expect(secondaryLexicalEditor).toBeHidden(); - }); - }); + // Lexical rendering tests moved to e2e/tests/admin/posts/lexical-editor.test.ts test.describe('Update post', () => { test.describe.configure({retries: 1}); @@ -680,38 +612,4 @@ test.describe('Updating post access', () => { }); }); -test.describe('Deleting a post', () => { - test('Delete a saved post', async ({page}) => { - await page.goto('/ghost'); - - await createPostDraft(page, {title: 'Delete a post test', body: 'This is the content'}); - - await expect(page.locator('[data-test-editor-post-status]')).toContainText('Draft - Saved'); - - await openPostSettingsMenu(page); - - await page.locator('[data-test-button="delete-post"]').click(); - - await page.locator('[data-test-button="delete-post-confirm"]').click(); - - await expect( - page.locator('[data-test-screen-title]') - ).toContainText('Posts'); - }); - - test('Delete a post with unsaved changes', async ({page}) => { - await page.goto('/ghost'); - - await createPostDraft(page, {title: 'Delete a post test', body: 'This is the content'}); - - await openPostSettingsMenu(page); - - await page.locator('[data-test-button="delete-post"]').click(); - - await page.locator('[data-test-button="delete-post-confirm"]').click(); - - await expect( - page.locator('[data-test-screen-title]') - ).toContainText('Posts'); - }); -}); +// Delete post tests moved to e2e/tests/admin/posts/publishing.test.ts From c3ba89f86d3283a0f77b4de270fa72967fa45081 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Fri, 27 Mar 2026 10:49:42 -0500 Subject: [PATCH 2/9] Fixed publishing e2e tests after local validation - Removed premature 'Draft - Saved' wait from createDraft (auto-save may not complete before publish flow opens, matching original behavior) - Added separate waitForSaved() method for tests that need it - Removed 'Published' status assertion after closing publish flow (close now navigates to posts list, not back to editor) --- e2e/helpers/pages/admin/posts/post/post-editor-page.ts | 4 ++++ e2e/tests/admin/posts/publishing.test.ts | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/e2e/helpers/pages/admin/posts/post/post-editor-page.ts b/e2e/helpers/pages/admin/posts/post/post-editor-page.ts index 91103e0c2d3..10eb40e5c06 100644 --- a/e2e/helpers/pages/admin/posts/post/post-editor-page.ts +++ b/e2e/helpers/pages/admin/posts/post/post-editor-page.ts @@ -121,6 +121,10 @@ export class PostEditorPage extends AdminPage { await this.page.keyboard.type(body); } + async waitForSaved(): Promise { + await this.postStatus.filter({hasText: /Saved/}).waitFor({timeout: 30000}); + } + get previewModalDesktopFrame(): DesktopPreviewFrame { return this.previewModal.desktopPreview; } diff --git a/e2e/tests/admin/posts/publishing.test.ts b/e2e/tests/admin/posts/publishing.test.ts index e6143f9818d..927a5cc91af 100644 --- a/e2e/tests/admin/posts/publishing.test.ts +++ b/e2e/tests/admin/posts/publishing.test.ts @@ -20,14 +20,11 @@ test.describe('Ghost Admin - Publishing', () => { const editor = new PostEditorPage(page); await editor.createDraft(postData); - await expect(editor.postStatus).toContainText('Draft - Saved'); await editor.publishFlow.open(); await editor.publishFlow.confirm(); await editor.publishFlow.close(); - await expect(editor.postStatus).toContainText('Published'); - const postPage = new PostPage(page); await postPage.goto(`/${generateSlug(postData.title)}/`); await expect(postPage.articleTitle).toHaveText(postData.title); @@ -51,7 +48,6 @@ test.describe('Ghost Admin - Publishing', () => { const editor = new PostEditorPage(page); await editor.createDraft(postData); - await expect(editor.postStatus).toContainText('Draft - Saved'); await editor.publishFlow.open(); await editor.publishFlow.selectPublishType('publish+send'); @@ -80,7 +76,6 @@ test.describe('Ghost Admin - Publishing', () => { const editor = new PostEditorPage(page); await editor.createDraft(postData); - await expect(editor.postStatus).toContainText('Draft - Saved'); await editor.publishFlow.open(); await editor.publishFlow.selectPublishType('send'); @@ -100,7 +95,7 @@ test.describe('Ghost Admin - Deleting Posts', () => { const editor = new PostEditorPage(page); await editor.createDraft({title: 'Delete a post test', body: 'This is the content'}); - await expect(editor.postStatus).toContainText('Draft - Saved'); + await editor.waitForSaved(); await editor.settingsToggleButton.click(); await editor.settingsMenu.deletePost(); From 2c2817b1cb6548e03cd40f8eedf38d31e30f6ad9 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 30 Mar 2026 10:24:45 -0500 Subject: [PATCH 3/9] upd --- e2e/tests/admin/posts/publishing.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/e2e/tests/admin/posts/publishing.test.ts b/e2e/tests/admin/posts/publishing.test.ts index 927a5cc91af..4d581fc5818 100644 --- a/e2e/tests/admin/posts/publishing.test.ts +++ b/e2e/tests/admin/posts/publishing.test.ts @@ -3,6 +3,9 @@ import {PostEditorPage, PostsPage} from '@/admin-pages'; import {PostPage} from '@/helpers/pages'; import {createMemberFactory, generateSlug} from '@/data-factory'; import {expect, test} from '@/helpers/playwright'; +import {usePerTestIsolation} from '@/helpers/playwright/isolation'; + +usePerTestIsolation(); async function getNewsletters(request: APIRequestContext): Promise<{id: string}[]> { const response = await request.get('/ghost/api/admin/newsletters/?status=active&limit=all'); @@ -94,8 +97,9 @@ test.describe('Ghost Admin - Deleting Posts', () => { await postsPage.newPostButton.click(); const editor = new PostEditorPage(page); - await editor.createDraft({title: 'Delete a post test', body: 'This is the content'}); - await editor.waitForSaved(); + 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(); From 6317992e9caa45e71a88cca121d26a657b402c90 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 30 Mar 2026 10:27:29 -0500 Subject: [PATCH 4/9] rm isolation --- e2e/tests/admin/posts/publishing.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/e2e/tests/admin/posts/publishing.test.ts b/e2e/tests/admin/posts/publishing.test.ts index 4d581fc5818..87bc4dcc9cb 100644 --- a/e2e/tests/admin/posts/publishing.test.ts +++ b/e2e/tests/admin/posts/publishing.test.ts @@ -3,9 +3,6 @@ import {PostEditorPage, PostsPage} from '@/admin-pages'; import {PostPage} from '@/helpers/pages'; import {createMemberFactory, generateSlug} from '@/data-factory'; import {expect, test} from '@/helpers/playwright'; -import {usePerTestIsolation} from '@/helpers/playwright/isolation'; - -usePerTestIsolation(); async function getNewsletters(request: APIRequestContext): Promise<{id: string}[]> { const response = await request.get('/ghost/api/admin/newsletters/?status=active&limit=all'); From 2e03eb01763e092930f74b684ed4ed3fd68ed9c2 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 30 Mar 2026 10:30:10 -0500 Subject: [PATCH 5/9] fix timeout --- .../admin/posts/post/post-editor-page.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/e2e/helpers/pages/admin/posts/post/post-editor-page.ts b/e2e/helpers/pages/admin/posts/post/post-editor-page.ts index 10eb40e5c06..5b907f3d5a3 100644 --- a/e2e/helpers/pages/admin/posts/post/post-editor-page.ts +++ b/e2e/helpers/pages/admin/posts/post/post-editor-page.ts @@ -1,7 +1,7 @@ import {AdminPage} from '@/admin-pages'; import {BasePage} from '@/helpers/pages'; import {DesktopPreviewFrame,PostPreviewModal} from '@/helpers/pages'; -import {Locator, Page} from '@playwright/test'; +import {expect, Locator, Page} from '@playwright/test'; class SettingsMenu extends BasePage { readonly postUrlInput: Locator; @@ -113,11 +113,24 @@ export class PostEditorPage extends AdminPage { } async createDraft({title = 'Hello world', body = 'This is my post body.'} = {}): Promise { + const editor = this.page.locator('[data-lexical-editor="true"]').first(); + await this.titleInput.click(); await this.titleInput.fill(title); - await this.page.locator('[data-lexical-editor="true"]').first().waitFor({state: 'visible'}); + await editor.waitFor({state: 'visible'}); await this.page.keyboard.press('Enter'); - await this.page.waitForTimeout(100); + + await expect.poll(async () => { + return await editor.evaluate((element) => { + const activeElement = document.activeElement; + + return Boolean( + activeElement && + (activeElement === element || element.contains(activeElement)) + ); + }); + }).toBe(true); + await this.page.keyboard.type(body); } From 4cf54af5a3355dae89747819270ed56c42fb8a2a Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 30 Mar 2026 11:00:52 -0500 Subject: [PATCH 6/9] tmp hook repro --- .../admin/posts/post/post-editor-page.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/e2e/helpers/pages/admin/posts/post/post-editor-page.ts b/e2e/helpers/pages/admin/posts/post/post-editor-page.ts index 5b907f3d5a3..480c7593a4e 100644 --- a/e2e/helpers/pages/admin/posts/post/post-editor-page.ts +++ b/e2e/helpers/pages/admin/posts/post/post-editor-page.ts @@ -120,16 +120,19 @@ export class PostEditorPage extends AdminPage { await editor.waitFor({state: 'visible'}); await this.page.keyboard.press('Enter'); - await expect.poll(async () => { - return await editor.evaluate((element) => { - const activeElement = document.activeElement; - - return Boolean( - activeElement && - (activeElement === element || element.contains(activeElement)) - ); - }); - }).toBe(true); + 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); } From 6dba23558fd111715228527ea37d8e77960afe77 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 30 Mar 2026 11:31:17 -0500 Subject: [PATCH 7/9] fix linting --- .lintstagedrc.cjs | 2 +- e2e/helpers/pages/admin/posts/post/post-editor-page.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs index b5ce3dcd45c..fb8d152975a 100644 --- a/.lintstagedrc.cjs +++ b/.lintstagedrc.cjs @@ -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) { diff --git a/e2e/helpers/pages/admin/posts/post/post-editor-page.ts b/e2e/helpers/pages/admin/posts/post/post-editor-page.ts index 480c7593a4e..4fe6d8e9725 100644 --- a/e2e/helpers/pages/admin/posts/post/post-editor-page.ts +++ b/e2e/helpers/pages/admin/posts/post/post-editor-page.ts @@ -1,7 +1,7 @@ import {AdminPage} from '@/admin-pages'; import {BasePage} from '@/helpers/pages'; import {DesktopPreviewFrame,PostPreviewModal} from '@/helpers/pages'; -import {expect, Locator, Page} from '@playwright/test'; +import {Locator, Page} from '@playwright/test'; class SettingsMenu extends BasePage { readonly postUrlInput: Locator; From b9fd05d38f8939966b71b3d16ac07ba4923dc764 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 30 Mar 2026 11:50:53 -0500 Subject: [PATCH 8/9] upd --- e2e/tests/admin/posts/publishing.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/tests/admin/posts/publishing.test.ts b/e2e/tests/admin/posts/publishing.test.ts index 87bc4dcc9cb..01c654741d9 100644 --- a/e2e/tests/admin/posts/publishing.test.ts +++ b/e2e/tests/admin/posts/publishing.test.ts @@ -4,10 +4,10 @@ import {PostPage} from '@/helpers/pages'; import {createMemberFactory, generateSlug} from '@/data-factory'; import {expect, test} from '@/helpers/playwright'; -async function getNewsletters(request: APIRequestContext): Promise<{id: string}[]> { +async function getNewsletters(request: APIRequestContext): Promise { 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}) => ({id: n.id})); + return data.newsletters.map((n: {id: string}) => n.id); } test.describe('Ghost Admin - Publishing', () => { @@ -39,7 +39,7 @@ test.describe('Ghost Admin - Publishing', () => { await memberFactory.create({ email: 'publish-email-test@example.com', name: 'Publishing member', - newsletters: newsletters as never + newsletters }); const postsPage = new PostsPage(page); @@ -67,7 +67,7 @@ test.describe('Ghost Admin - Publishing', () => { await memberFactory.create({ email: 'email-only-test@example.com', name: 'Publishing member', - newsletters: newsletters as never + newsletters }); const postsPage = new PostsPage(page); From b170f3c2c7e1858d37c701a26db99ef219ce2ea8 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 30 Mar 2026 12:19:14 -0500 Subject: [PATCH 9/9] enable mailgun --- e2e/tests/admin/posts/publishing.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/tests/admin/posts/publishing.test.ts b/e2e/tests/admin/posts/publishing.test.ts index 01c654741d9..ed82c843dfd 100644 --- a/e2e/tests/admin/posts/publishing.test.ts +++ b/e2e/tests/admin/posts/publishing.test.ts @@ -11,6 +11,8 @@ async function getNewsletters(request: APIRequestContext): Promise { } 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.'};