Skip to content

Moved core publishing browser tests to e2e suite (slice 1/4)#27004

Open
9larsons wants to merge 2 commits intomainfrom
migrate-publishing-e2e-slice-1
Open

Moved core publishing browser tests to e2e suite (slice 1/4)#27004
9larsons wants to merge 2 commits intomainfrom
migrate-publishing-e2e-slice-1

Conversation

@9larsons
Copy link
Contributor

Summary

  • 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

Context

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.ts passes
  • cd e2e && yarn test tests/admin/posts/lexical-editor.test.ts passes
  • cd e2e && yarn lint passes (0 new errors)
  • cd e2e && yarn test:types passes
  • Remaining 13 browser tests in publishing.spec.js still pass

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.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 27, 2026

Walkthrough

End-to-end test code was refactored: new helper locators and methods were added to admin post editor helpers (PostEditorPage, PublishFlow, SettingsMenu) and the public PostPage. New Playwright test files were added to cover Lexical editor rendering, post publishing variants (publish, publish+send, send-only), and post deletion flows. Corresponding in-file tests were removed from the legacy publishing.spec.js and replaced by comments noting the move.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: moving core publishing browser tests from the legacy e2e-browser suite to the modern e2e suite, specifically as slice 1 of 4.
Description check ✅ Passed The description provides comprehensive context, details all changes made, explains the migration scope, and includes a clear test plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch migrate-publishing-e2e-slice-1

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
e2e/tests/admin/posts/publishing.test.ts (2)

42-46: Type mismatch hidden by as never cast.

The newsletters parameter expects string[] per the MemberFactory interface, but getNewsletters() returns {id: string}[]. The as never cast 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 MemberFactory interface 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

📥 Commits

Reviewing files that changed from the base of the PR and between d28725e and b6a7cf1.

📒 Files selected for processing (5)
  • e2e/helpers/pages/admin/posts/post/post-editor-page.ts
  • e2e/helpers/pages/public/post-page.ts
  • e2e/tests/admin/posts/lexical-editor.test.ts
  • e2e/tests/admin/posts/publishing.test.ts
  • ghost/core/test/e2e-browser/admin/publishing.spec.js

Comment on lines +115 to +122
async createDraft({title = 'Hello world', body = 'This is my post body.'} = {}): Promise<void> {
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid hard-coded waitForTimeout in createDraft().

Line 120 uses page.waitForTimeout(100) which violates the coding guidelines. The comment in the legacy test explains this wait allows the "new->draft switch to occur fully," but this introduces flakiness.

Consider replacing with a proper wait condition, such as waiting for the editor state to stabilize or for a specific element attribute change.

Suggested fix using a more reliable wait
 async createDraft({title = 'Hello world', body = 'This is my post body.'} = {}): Promise<void> {
     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);
+    // Wait for the editor to be ready for input after the title->body transition
+    await this.page.locator('[data-lexical-editor="true"]').first().waitFor({state: 'visible'});
     await this.page.keyboard.type(body);
 }

As per coding guidelines: "Do not use hard-coded waits (waitForTimeout)".

🤖 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 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.

@9larsons 9larsons enabled auto-merge (squash) March 27, 2026 15:22
@github-actions
Copy link
Contributor

E2E Tests Failed

To 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)
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
27.1% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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:

  1. Adding await editor.publishFlow.close() for consistency, or
  2. 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: The as never type assertion bypasses type safety.

This cast suggests a type mismatch between the newsletter structure from the API and what memberFactory.create expects. 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 never in 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

📥 Commits

Reviewing files that changed from the base of the PR and between b6a7cf1 and c3ba89f.

📒 Files selected for processing (2)
  • e2e/helpers/pages/admin/posts/post/post-editor-page.ts
  • e2e/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

@github-actions
Copy link
Contributor

E2E Tests Failed

To 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"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant