Skip to content
Open
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
142 changes: 74 additions & 68 deletions e2e/tests/admin/posts/publishing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,9 @@ async function expectFrontendStatus(page: Page, slug: string, status: number, ti
}).toBe(status);
}

function formatDateInput(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');

return `${year}-${month}-${day}`;
}

function getAsapSchedule() {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

function getFutureSchedule() {
return {
date: formatDateInput(yesterday),
time: '00:00'
date: '2050-01-01'
Comment on lines +23 to +25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use one schedule source of truth and validate it in the waiter.

published_at only has to be some string here, so a request with the wrong scheduled day/time can still resolve the wait. Because the tests later hard-code 2050 in their UI assertions, the input and the checks can drift apart. Return the expected date/year from getFutureSchedule() and pass the date into this helper so the predicate verifies the exact schedule value.

🎯 Suggested shape
 function getFutureSchedule() {
     return {
-        date: '2050-01-01'
+        date: '2050-01-01',
+        year: '2050'
     };
 }

-async function waitForScheduledSaveResponse(page: Page, resource: 'posts' | 'pages') {
+async function waitForScheduledSaveResponse(page: Page, resource: 'posts' | 'pages', scheduledDate: string) {
@@
                 const resourcePayload = item as Record<string, unknown>;
                 return resourcePayload.status === 'scheduled' ||
                     (typeof resourcePayload.published_at === 'string' &&
+                        resourcePayload.published_at.startsWith(scheduledDate) &&
                         resourcePayload.status !== 'published' &&
                         resourcePayload.status !== 'sent');
             });

Then reuse the returned date/year in the scheduled test call sites and status assertions.

Also applies to: 46-80

🤖 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 23 - 25,
getFutureSchedule currently returns a hard-coded date string that tests don't
reuse, letting the waiter predicate pass with the wrong scheduled value; change
getFutureSchedule() to return a single source-of-truth object (e.g., { date,
year }) and update all call sites to pass date into the waiter/helper that
asserts published_at so the predicate verifies the exact scheduled date/year
(update references in the scheduled post test and the waiter/predicate function
to compare against the returned date/year from getFutureSchedule()).

};
}

Expand All @@ -55,6 +43,49 @@ async function expectPostStatus(editor: PostEditorPage, status: string | RegExp,
}
}

async function waitForScheduledSaveResponse(page: Page, resource: 'posts' | 'pages') {
const response = await page.waitForResponse((networkResponse) => {
if (networkResponse.request().method() !== 'PUT' || networkResponse.status() !== 200) {
return false;
}

const pathname = new URL(networkResponse.url()).pathname;
if (!new RegExp(`/ghost/api/admin/${resource}/[^/]+/?$`).test(pathname)) {
return false;
}

const postData = networkResponse.request().postData();
if (!postData) {
return false;
}

try {
const payload = JSON.parse(postData) as Record<string, unknown>;
const resources = payload[resource];

if (!Array.isArray(resources)) {
return false;
}

return resources.some((item) => {
if (!item || typeof item !== 'object') {
return false;
}

const resourcePayload = item as Record<string, unknown>;
return resourcePayload.status === 'scheduled' ||
(typeof resourcePayload.published_at === 'string' &&
resourcePayload.status !== 'published' &&
resourcePayload.status !== 'sent');
});
} catch {
return false;
}
});

expect(response.status()).toBe(200);
}

function buildLexicalWithBody(body: string): string {
return JSON.stringify({
root: {
Expand Down Expand Up @@ -206,7 +237,7 @@ test.describe('Ghost Admin - Publishing', () => {
await expectFrontendStatus(frontendPage, slug, 404);
});

test.skip('scheduled publish only - post becomes visible on frontend', async ({page}) => {
test('scheduled publish only - post is scheduled', async ({page}) => {
const title = `scheduled-publish-only-${Date.now()}`;
const body = 'This is my scheduled post body.';
const slug = generateSlug(title);
Expand All @@ -219,26 +250,20 @@ test.describe('Ghost Admin - Publishing', () => {
await editor.createDraft({title, body});

await editor.publishFlow.open();
await editor.publishFlow.schedule(getAsapSchedule());
await editor.publishFlow.confirm();
await editor.publishFlow.schedule(getFutureSchedule());
await Promise.all([
waitForScheduledSaveResponse(page, 'posts'),
editor.publishFlow.confirm()
]);
await editor.publishFlow.close();

await expect(editor.postStatus.first()).toContainText('Scheduled');
await expectFrontendStatus(page, slug, 200, 20000);
await expectPostStatus(editor, 'Scheduled', /to be published\s+at .*2050/i);

const frontendPage = await page.context().newPage();
const publicPage = new PostPage(frontendPage);

await publicPage.gotoPost(slug);
await expect(publicPage.articleTitle).toHaveText(title);
await expect(publicPage.articleBody).toHaveText(body);

await postsPage.goto();
await postsPage.getPostByTitle(title).click();
await expectPostStatus(editor, 'Published');
await expectFrontendStatus(frontendPage, slug, 404);
});

test.skip('scheduled publish and email - post becomes visible on frontend', async ({page}) => {
test('scheduled publish and email - post is scheduled', async ({page}) => {
const title = `scheduled-publish-email-${Date.now()}`;
const body = 'This is my scheduled publish and email post body.';
const slug = generateSlug(title);
Expand All @@ -260,29 +285,21 @@ test.describe('Ghost Admin - Publishing', () => {

await editor.publishFlow.open();
await editor.publishFlow.selectPublishType('publish+send');
await editor.publishFlow.schedule(getAsapSchedule());
await editor.publishFlow.confirm();
await editor.publishFlow.schedule(getFutureSchedule());
await Promise.all([
waitForScheduledSaveResponse(page, 'posts'),
editor.publishFlow.confirm()
]);
await editor.publishFlow.close();

await expectPostStatus(editor, 'Scheduled', /published and sent/i);
await expectPostStatus(editor, 'Scheduled', /few seconds/i);
await expectPostStatus(editor, 'Scheduled', /2050/i);

const frontendPage = await page.context().newPage();
const publicPage = new PostPage(frontendPage);

await expectFrontendStatus(frontendPage, slug, 404);
await expectFrontendStatus(frontendPage, slug, 200, 20000);

await publicPage.gotoPost(slug);
await expect(publicPage.articleTitle).toHaveText(title);
await expect(publicPage.articleBody).toHaveText(body);

await postsPage.goto();
await postsPage.getPostByTitle(title).click();
await expectPostStatus(editor, 'Published');
});

test.skip('scheduled email only - post is not visible on frontend', async ({page}) => {
test('scheduled email only - post is scheduled and not visible on frontend', async ({page}) => {
const title = `scheduled-email-only-${Date.now()}`;
const body = 'This is my scheduled email-only post body.';
const slug = generateSlug(title);
Expand All @@ -304,28 +321,21 @@ test.describe('Ghost Admin - Publishing', () => {

await editor.publishFlow.open();
await editor.publishFlow.selectPublishType('send');
await editor.publishFlow.schedule(getAsapSchedule());
await editor.publishFlow.confirm();
await editor.publishFlow.schedule(getFutureSchedule());
await Promise.all([
waitForScheduledSaveResponse(page, 'posts'),
editor.publishFlow.confirm()
]);
await editor.publishFlow.close();

await expectPostStatus(editor, 'Scheduled', /to be sent/i);
await expectPostStatus(editor, 'Scheduled', /2050/i);

const frontendPage = await page.context().newPage();
await expectFrontendStatus(frontendPage, slug, 404);

await postsPage.goto();
await postsPage.getPostByTitle(title).click();
await expect.poll(async () => {
await page.reload();
return await editor.postStatus.first().textContent();
}, {
timeout: 15000
}).toContain('Sent');
await expectPostStatus(editor, 'Sent', /Sent\s+to/i);
await expectFrontendStatus(frontendPage, slug, 404);
});

test.skip('scheduled publish only - page becomes visible on frontend', async ({page}) => {
test('scheduled publish only - page is scheduled', async ({page}) => {
const title = `scheduled-page-only-${Date.now()}`;
const body = 'This is my scheduled page body.';
const slug = generateSlug(title);
Expand All @@ -335,21 +345,17 @@ test.describe('Ghost Admin - Publishing', () => {
await editor.createDraft({title, body});

await editor.publishFlow.open();
await editor.publishFlow.schedule(getAsapSchedule());
await editor.publishFlow.confirm();
await editor.publishFlow.schedule(getFutureSchedule());
await Promise.all([
waitForScheduledSaveResponse(page, 'pages'),
editor.publishFlow.confirm()
]);
await editor.publishFlow.close();

await expectPostStatus(editor, 'Scheduled', /few seconds/i);
await expectPostStatus(editor, 'Scheduled', /to be published\s+at .*2050/i);

const frontendPage = await page.context().newPage();
const publicPage = new PostPage(frontendPage);

await expectFrontendStatus(frontendPage, slug, 404);
await expectFrontendStatus(frontendPage, slug, 200, 20000);

await publicPage.gotoPost(slug);
await expect(publicPage.articleTitle).toHaveText(title);
await expect(publicPage.articleBody).toHaveText(body);
});

test('publish only - page is visible on frontend', async ({page}) => {
Expand Down
Loading