Moved core publishing browser tests to e2e - #27004
Conversation
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.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdded new locators and actions to admin post helpers: 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
e2e/tests/admin/posts/publishing.test.ts (2)
42-46: Type mismatch hidden byas nevercast.The
newslettersparameter expectsstring[]per theMemberFactoryinterface, butgetNewsletters()returns{id: string}[]. Theas nevercast bypasses TypeScript's type checking. This works at runtime because the API adapter handles the transformation, but it's fragile.Consider updating the type signature or using a more explicit cast:
Suggested improvement
await memberFactory.create({ email: 'publish-email-test@example.com', name: 'Publishing member', - newsletters: newsletters as never + newsletters: newsletters as unknown as string[] });Or better, update the
MemberFactoryinterface to accept{id: string}[]for newsletters since that's what the API expects.Also applies to: 71-75
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/tests/admin/posts/publishing.test.ts` around lines 42 - 46, The test is hiding a type mismatch by using `as never` when calling `memberFactory.create`; `getNewsletters()` returns `{id: string}[]` but `MemberFactory` currently expects `string[]`. Fix by either updating the `MemberFactory` interface to accept `Array<{id: string}>` for the `newsletters` parameter (so `memberFactory.create` and tests pass naturally) or explicitly map the result of `getNewsletters()` to a `string[]` (e.g., `getNewsletters().map(n => n.id)`) before passing it to `memberFactory.create`; update calls at the two test sites (the call at `memberFactory.create` around lines ~42 and the similar one around lines ~71) and adjust types on the `MemberFactory` definition accordingly to keep typesafe code.
7-11: Consider adding error handling for the API call.The
getNewsletters()helper doesn't check the response status before parsing JSON. If the API call fails, this could produce confusing errors.Suggested improvement
async function getNewsletters(request: APIRequestContext): Promise<{id: string}[]> { const response = await request.get('/ghost/api/admin/newsletters/?status=active&limit=all'); + if (!response.ok()) { + throw new Error(`Failed to fetch newsletters: ${response.status()}`); + } const data = await response.json(); return data.newsletters.map((n: {id: string}) => ({id: n.id})); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/tests/admin/posts/publishing.test.ts` around lines 7 - 11, The getNewsletters helper currently calls request.get and immediately parses response.json which can produce confusing errors on non-2xx responses; update getNewsletters to check response.ok (or response.status) after request.get('/ghost/api/admin/newsletters/?status=active&limit=all') and if not ok throw or return a clear error that includes response.status and response.statusText (optionally include the response body for debugging by awaiting response.text()), otherwise proceed to await response.json() and map data.newsletters to ({id: n.id}) as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@e2e/helpers/pages/admin/posts/post/post-editor-page.ts`:
- Around line 115-122: The hard-coded waitForTimeout(100) inside createDraft()
causes flakiness; replace it with an explicit wait that asserts the editor has
completed the new->draft transition before typing the body. Concretely, after
filling title and pressing Enter (in createDraft), wait on the editor locator
returned by page.locator('[data-lexical-editor="true"]').first() for a stable
condition such as: the editor becoming editable/focused, its innerText or child
paragraph element appearing/containing expected content, or a specific
attribute/class that indicates the draft state has been applied; then proceed
with page.keyboard.type(body). Use createDraft, titleInput, and the editor
locator to locate and wait for that deterministic condition instead of
waitForTimeout.
---
Nitpick comments:
In `@e2e/tests/admin/posts/publishing.test.ts`:
- Around line 42-46: The test is hiding a type mismatch by using `as never` when
calling `memberFactory.create`; `getNewsletters()` returns `{id: string}[]` but
`MemberFactory` currently expects `string[]`. Fix by either updating the
`MemberFactory` interface to accept `Array<{id: string}>` for the `newsletters`
parameter (so `memberFactory.create` and tests pass naturally) or explicitly map
the result of `getNewsletters()` to a `string[]` (e.g., `getNewsletters().map(n
=> n.id)`) before passing it to `memberFactory.create`; update calls at the two
test sites (the call at `memberFactory.create` around lines ~42 and the similar
one around lines ~71) and adjust types on the `MemberFactory` definition
accordingly to keep typesafe code.
- Around line 7-11: The getNewsletters helper currently calls request.get and
immediately parses response.json which can produce confusing errors on non-2xx
responses; update getNewsletters to check response.ok (or response.status) after
request.get('/ghost/api/admin/newsletters/?status=active&limit=all') and if not
ok throw or return a clear error that includes response.status and
response.statusText (optionally include the response body for debugging by
awaiting response.text()), otherwise proceed to await response.json() and map
data.newsletters to ({id: n.id}) as before.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a2a16031-7556-4f40-9baf-7e317fc4f284
📒 Files selected for processing (5)
e2e/helpers/pages/admin/posts/post/post-editor-page.tse2e/helpers/pages/public/post-page.tse2e/tests/admin/posts/lexical-editor.test.tse2e/tests/admin/posts/publishing.test.tsghost/core/test/e2e-browser/admin/publishing.spec.js
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 23652926430 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
- 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)
There was a problem hiding this comment.
🧹 Nitpick comments (3)
e2e/tests/admin/posts/publishing.test.ts (3)
52-59: Inconsistent publish flow closing between tests.The first test calls
editor.publishFlow.close()after confirming, but this test navigates directly without closing the modal. While this may work due to page navigation, the inconsistency could lead to flaky behavior if the modal interferes with navigation.Consider either:
- Adding
await editor.publishFlow.close()for consistency, or- Documenting why it's intentionally omitted here
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/tests/admin/posts/publishing.test.ts` around lines 52 - 59, The publish flow modal is left open in this test which is inconsistent with the first test; after calling editor.publishFlow.confirm() you should explicitly close the modal to avoid flakiness by calling await editor.publishFlow.close() before navigating to the PostPage (refer to editor.publishFlow.confirm(), editor.publishFlow.close(), publishFlow.selectPublishType, PostPage, generateSlug and postData in this block), so add the close call immediately after confirm() or add a short comment explaining why the modal remaining open is intentional.
7-11: Add defensive error handling for the API response.The helper assumes a successful response with the expected structure. If the API fails or returns an unexpected shape, the error message will be unclear (e.g., "Cannot read properties of undefined").
🛡️ Suggested improvement
async function getNewsletters(request: APIRequestContext): Promise<{id: string}[]> { const response = await request.get('/ghost/api/admin/newsletters/?status=active&limit=all'); + if (!response.ok()) { + throw new Error(`Failed to fetch newsletters: ${response.status()}`); + } const data = await response.json(); + if (!data.newsletters?.length) { + throw new Error('No active newsletters found - tests require at least one newsletter'); + } return data.newsletters.map((n: {id: string}) => ({id: n.id})); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/tests/admin/posts/publishing.test.ts` around lines 7 - 11, The helper getNewsletters currently assumes a successful response and valid shape; add defensive checks: after request.get('/ghost/api/admin/newsletters/?status=active&limit=all') verify response.ok and if not throw a descriptive Error that includes response.status and statusText (and optionally response.text()); parse JSON and ensure the parsed object has a newsletters array (Array.isArray(data.newsletters)); if the shape is invalid throw a clear Error describing the unexpected payload; finally return the mapped ids only when validation passes.
39-43: Theas nevertype assertion bypasses type safety.This cast suggests a type mismatch between the newsletter structure from the API and what
memberFactory.createexpects. Consider aligning the types or using a proper type assertion.♻️ Suggested approach
If the factory expects a different newsletter type, define an explicit interface and map accordingly:
- await memberFactory.create({ - email: 'publish-email-test@example.com', - name: 'Publishing member', - newsletters: newsletters as never - }); + await memberFactory.create({ + email: 'publish-email-test@example.com', + name: 'Publishing member', + newsletters + });If the factory's type definition is incorrect, updating the factory's type signature would be cleaner than using
as neverin multiple places.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/tests/admin/posts/publishing.test.ts` around lines 39 - 43, The test is skipping type checking by using "newsletters as never" when calling memberFactory.create; update the call to pass a correctly typed value instead of casting to never: either convert/map the existing newsletters variable to the shape expected by memberFactory.create (create a small mapper or explicit interface to transform newsletter objects) or fix the factory's type signature so it accepts the API newsletter type; locate the memberFactory.create invocation and the newsletters definition and replace the "as never" cast with a proper typed object or adjusted factory type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@e2e/tests/admin/posts/publishing.test.ts`:
- Around line 52-59: The publish flow modal is left open in this test which is
inconsistent with the first test; after calling editor.publishFlow.confirm() you
should explicitly close the modal to avoid flakiness by calling await
editor.publishFlow.close() before navigating to the PostPage (refer to
editor.publishFlow.confirm(), editor.publishFlow.close(),
publishFlow.selectPublishType, PostPage, generateSlug and postData in this
block), so add the close call immediately after confirm() or add a short comment
explaining why the modal remaining open is intentional.
- Around line 7-11: The helper getNewsletters currently assumes a successful
response and valid shape; add defensive checks: after
request.get('/ghost/api/admin/newsletters/?status=active&limit=all') verify
response.ok and if not throw a descriptive Error that includes response.status
and statusText (and optionally response.text()); parse JSON and ensure the
parsed object has a newsletters array (Array.isArray(data.newsletters)); if the
shape is invalid throw a clear Error describing the unexpected payload; finally
return the mapped ids only when validation passes.
- Around line 39-43: The test is skipping type checking by using "newsletters as
never" when calling memberFactory.create; update the call to pass a correctly
typed value instead of casting to never: either convert/map the existing
newsletters variable to the shape expected by memberFactory.create (create a
small mapper or explicit interface to transform newsletter objects) or fix the
factory's type signature so it accepts the API newsletter type; locate the
memberFactory.create invocation and the newsletters definition and replace the
"as never" cast with a proper typed object or adjusted factory type.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 834d5ef0-fb28-452c-83df-d0e5affda203
📒 Files selected for processing (2)
e2e/helpers/pages/admin/posts/post/post-editor-page.tse2e/tests/admin/posts/publishing.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- e2e/helpers/pages/admin/posts/post/post-editor-page.ts
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 23654932768 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
e2e
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@e2e/helpers/pages/admin/posts/post/post-editor-page.ts`:
- Line 4: Replace the assertion-style expect.poll() in the PostEditorPage page
object with a guard using page.waitForFunction(): remove the expect.poll() call
and instead call await this.page.waitForFunction(...) (or
this.page.waitForFunction bound to the Page instance used in the page object)
with the same predicate and timeout so the page object only waits for the
condition rather than asserting; update the method that currently contains
expect.poll() to return after the wait completes (preserve any timeout/options)
and remove any expect-based error handling from that page object method.
In `@e2e/tests/admin/posts/publishing.test.ts`:
- Around line 39-43: The test currently bypasses type checking by casting
newsletters to never when calling memberFactory.create; instead, remove the "as
never" cast and map the newsletter objects returned by getNewsletters() to a
string[] of IDs (e.g., newsletters.map(n => n.id)) before passing into
memberFactory.create (reference the newsletters variable and the
memberFactory.create call); apply the same change to the second occurrence
around the other memberFactory.create call so both pass a string[] to the
Member.newsletters parameter.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ab1350d1-c670-4503-ac17-b09e58d2393c
📒 Files selected for processing (2)
e2e/helpers/pages/admin/posts/post/post-editor-page.tse2e/tests/admin/posts/publishing.test.ts
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 23752993036 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
e2e/helpers/pages/admin/posts/post/post-editor-page.ts (2)
10-11: Make newly added locators explicitlypublic readonly.These new locators are currently
readonlyonly. Please align them with the page-object visibility rule.♻️ Suggested update
- readonly deletePostButton: Locator; - readonly deletePostConfirmButton: Locator; + public readonly deletePostButton: Locator; + public readonly deletePostConfirmButton: Locator; ... - readonly closeButton: Locator; - readonly completeBookmark: Locator; + public readonly closeButton: Locator; + public readonly completeBookmark: Locator; ... - readonly screenTitle: Locator; - readonly lexicalEditor: Locator; - readonly secondaryEditor: Locator; + public readonly screenTitle: Locator; + public readonly lexicalEditor: Locator; + public readonly secondaryEditor: Locator;As per coding guidelines: "Page Objects should be located in
helpers/pages/and expose locators aspublic readonly".Also applies to: 36-37, 87-89
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/helpers/pages/admin/posts/post/post-editor-page.ts` around lines 10 - 11, The new locators are declared as readonly but must follow the page-object visibility rule and be declared public readonly; update the declarations for deletePostButton and deletePostConfirmButton (and the other affected locator declarations around the indicated areas, e.g., the members at lines referenced like the ones near 36-37 and 87-89) to use the public readonly modifier so the class exposes them correctly (locate the declarations by the symbol names deletePostButton and deletePostConfirmButton and change their signatures to public readonly).
123-135: Use locatorwaitFor()guard here instead ofpage.waitForFunction().In page objects, guards should stay on locator
waitFor()rather than function polling.♻️ Suggested update
- 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 editor.locator(':focus-within').waitFor({state: 'visible'});As per coding guidelines: "Use
waitFor()for guards in page objects, neverexpect()in page objects".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/helpers/pages/admin/posts/post/post-editor-page.ts` around lines 123 - 135, Replace the use of this.page.waitForFunction polling with a locator-based guard: locate the editor via the selector '[data-lexical-editor="true"]' and call the locator's waitFor() to wait for it to be attached/visible, then use the locator API (e.g., locator.isFocused() or locator.evaluate(()=>document.activeElement) ) to verify focus; specifically change the this.page.waitForFunction call to await this.page.locator('[data-lexical-editor="true"]').waitFor(...) and then check focus using the same locator rather than polling with page.waitForFunction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@e2e/helpers/pages/admin/posts/post/post-editor-page.ts`:
- Around line 10-11: The new locators are declared as readonly but must follow
the page-object visibility rule and be declared public readonly; update the
declarations for deletePostButton and deletePostConfirmButton (and the other
affected locator declarations around the indicated areas, e.g., the members at
lines referenced like the ones near 36-37 and 87-89) to use the public readonly
modifier so the class exposes them correctly (locate the declarations by the
symbol names deletePostButton and deletePostConfirmButton and change their
signatures to public readonly).
- Around line 123-135: Replace the use of this.page.waitForFunction polling with
a locator-based guard: locate the editor via the selector
'[data-lexical-editor="true"]' and call the locator's waitFor() to wait for it
to be attached/visible, then use the locator API (e.g., locator.isFocused() or
locator.evaluate(()=>document.activeElement) ) to verify focus; specifically
change the this.page.waitForFunction call to await
this.page.locator('[data-lexical-editor="true"]').waitFor(...) and then check
focus using the same locator rather than polling with page.waitForFunction.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 31efacf3-b050-46f4-bc39-64d210f58fa8
📒 Files selected for processing (2)
.lintstagedrc.cjse2e/helpers/pages/admin/posts/post/post-editor-page.ts
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 23755814930 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
e2e/tests/admin/posts/publishing.test.ts (1)
7-11: Consider adding error handling togetNewslettersfor more informative test failures.If the API call fails or returns unexpected data (e.g.,
data.newslettersis undefined), this function will throw a cryptic error. Adding basic validation would improve debuggability.🛡️ Optional improvement for robustness
async function getNewsletters(request: APIRequestContext): Promise<string[]> { const response = await request.get('/ghost/api/admin/newsletters/?status=active&limit=all'); + if (!response.ok()) { + throw new Error(`Failed to fetch newsletters: ${response.status()}`); + } const data = await response.json(); + if (!data.newsletters?.length) { + throw new Error('No active newsletters found'); + } return data.newsletters.map((n: {id: string}) => n.id); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/tests/admin/posts/publishing.test.ts` around lines 7 - 11, getNewsletters currently assumes the request succeeds and data.newsletters exists; add basic error handling in getNewsletters to make failures informative: after awaiting request.get, check response.ok and throw an error including response.status and text if not ok; after parsing JSON into data, validate that data.newsletters is an array (and throw a descriptive error showing the parsed data if not), then return data.newsletters.map as before (referencing getNewsletters, response, data, and the mapping to n.id).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@e2e/tests/admin/posts/publishing.test.ts`:
- Around line 7-11: getNewsletters currently assumes the request succeeds and
data.newsletters exists; add basic error handling in getNewsletters to make
failures informative: after awaiting request.get, check response.ok and throw an
error including response.status and text if not ok; after parsing JSON into
data, validate that data.newsletters is an array (and throw a descriptive error
showing the parsed data if not), then return data.newsletters.map as before
(referencing getNewsletters, response, data, and the mapping to n.id).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4a68eddb-5cf2-4e87-bc69-6467e3c07818
📒 Files selected for processing (1)
e2e/tests/admin/posts/publishing.test.ts
E2E Tests FailedTo view the Playwright test report locally, run: REPORT_DIR=$(mktemp -d) && gh run download 23756680185 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR" |
|
ref https://linear.app/ghost/issue/PLA-10/ - Migrated 7 deterministic publishing tests from `ghost/core/test/e2e-browser/admin/publishing.spec.js` to the e2e suite - Tests moved: publish only, publish+email, email only, delete saved post, delete with unsaved changes, primary lexical editor, secondary hidden lexical editor - Extended `PostEditorPage` with publish flow close/delete methods, `createDraft()` helper, and lexical editor locators - Extended `PostPage` with `articleTitle` and `articleBody` locators
…t#27004) ref https://linear.app/ghost/issue/PLA-10/ - Migrated 7 deterministic publishing tests from `ghost/core/test/e2e-browser/admin/publishing.spec.js` to the e2e suite - Tests moved: publish only, publish+email, email only, delete saved post, delete with unsaved changes, primary lexical editor, secondary hidden lexical editor - Extended `PostEditorPage` with publish flow close/delete methods, `createDraft()` helper, and lexical editor locators - Extended `PostPage` with `articleTitle` and `articleBody` locators


Summary
ghost/core/test/e2e-browser/admin/publishing.spec.jsto the e2e suitePostEditorPagewith publish flow close/delete methods,createDraft()helper, and lexical editor locatorsPostPagewitharticleTitleandarticleBodylocatorsContext
This is slice 1 of 4 in an incremental migration of browser tests to the e2e suite. This slice covers the simplest, most deterministic tests — no scheduling waits, no visibility/access changes, no settings mutations. Remaining slices will cover publish page + update post, schedule post, and post access tests.
Test plan
cd e2e && yarn test tests/admin/posts/publishing.test.tspassescd e2e && yarn test tests/admin/posts/lexical-editor.test.tspassescd e2e && yarn lintpasses (0 new errors)cd e2e && yarn test:typespassespublishing.spec.jsstill pass