Skip to main content

How to Test CMS Content Publishing Workflows

Short answer

CMS workflows fail when draft content leaks to public CDN, scheduled publish never fires, preview tokens work after expiry, or rollback leaves stale cache—not when editors can save drafts. Probe content_state on draft, preview, and public endpoints; use Playwright clock for publish_at; pair rich text tests with sanitized HTML probes; enforce RBAC on publish actions.

Part of Testing Guides by industry.

Who this is for

Teams shipping marketing CMS, headless Contentful/Sanity/Strapi workflows, or in-app page builders with draft → review → publish, scheduled go-live, preview URLs, and rollback.

Why testing CMS publishing matters

Publishing mistakes are public and immediate:

  • Draft leak — preview or misconfigured CDN serves draft to Google; SEO and legal exposure.
  • Scheduled publish miss — campaign launches empty; revenue event failure.
  • Rollback incomplete — revert shows old title but body cached; brand inconsistency.
  • RBAC bypass — writer publishes without approver; governance failure.
  • Wrong locale publish — EN draft live on DE path; i18n incident.

Probe content_state, public cache headers, and slug resolution on three surfaces: editor API, preview route, anonymous public URL.

Complexity map

ScenarioEdge caseWhy tests breakApproach
Preview leakPublic CDNDraft indexedProbe cache + noindex
Scheduled publishCron lagStill draftclock + job trigger
RollbackCDN staleMixed versionProbe version id
RBAC publishWriter onlyPublishedProbe 403 + state draft
Preview token expiryOld link worksSecurityclock + 403
Slug changeOld URL 404Broken linksProbe redirect
Rich text bodyXSS in draftPublic exploitSanitizer probe
Multi-localeWrong lang liveMixedProbe locale field
UnpublishStill in sitemapSEO ghostProbe sitemap + state
Concurrent editLast save winsLost contentProbe version conflict
Asset referenceBroken imageLive 404Probe asset id
Scheduled unpublishContent remainsPolicyclock + probe archived

Seed draft content

await request.post('/api/test/seed-content', {
data: {
runId,
slug: `campaign-${runId}`,
state: 'draft',
bodyHtml: '<h1>Draft headline</h1>',
publishAt: null,
},
});

Draft must not appear publicly

const publicRes = await request.get(`https://test-site.local/p/${slug}`);
expect(publicRes.status()).toBe(404);

const preview = await request.get(`/api/test/preview-token?slug=${slug}`).then(r => r.json());
await page.goto(preview.url);
await expect(page.getByRole('heading')).toContainText('Draft headline');

const publicAgain = await request.get(`https://test-site.local/p/${slug}`);
expect(publicAgain.status()).toBe(404);

Publish and probe

await page.goto(`/cms/edit/${slug}`);
await page.getByRole('button', { name: 'Publish' }).click(); // as approver role
await expect.poll(async () =>
(await request.get(`/api/test/content?slug=${slug}`)).json()
).toMatchObject({ state: 'published' });

const live = await request.get(`https://test-site.local/p/${slug}`);
expect(live.status()).toBe(200);
expect(live.headers()['cache-control']).toMatch(/public/);

Pair editor body tests with rich text guide.

Scheduled publish

await request.patch(`/api/test/content/${slug}`, {
data: { publishAt: '2024-06-15T09:00:00Z', state: 'scheduled' },
});
await context.clock.install({ time: new Date('2024-06-15T08:59:00Z') });
await page.goto(`https://test-site.local/p/${slug}`);
expect((await page.goto(`https://test-site.local/p/${slug}`))!.status()).toBe(404);

await context.clock.runFor(60_000);
await request.post('/api/test/trigger-publish-jobs'); // or wait cron
await expect.poll(async () =>
(await request.get(`/api/test/content?slug=${slug}`)).json().state
).toBe('published');

RBAC negative

// Login as writer without publish permission
await page.getByRole('button', { name: 'Publish' }).click();
await expect(page.getByRole('alert')).toContainText(/permission/i);
const doc = await request.get(`/api/test/content?slug=${slug}`).then(r => r.json());
expect(doc.state).toBe('draft');

See RBAC permissions.

Requirement slices to cover

  • content_state — draft, scheduled, published, archived

CI checklist

  1. Public 404 while draft for every slug spec
  2. Preview uses token; anonymous cannot access
  3. Scheduled publish with clock + job trigger
  4. Rollback changes version id on public probe
  5. Rich text XSS paste negative on publish path
  6. Writer cannot publish without approver probe

Anti-patterns

Anti-patternWhy it failsBetter approach
Editor save onlyLeak untestedPublic URL probe
Assert CMS UI badgeCache staleversion id probe
Real CDN purge waitSlowTest purge hook
Skip scheduledLaunch bugsclock spec
One localeWrong lang livelocale probe
No RBAC negativeGovernance403 publish

Example scenario

Situation: Editor schedules campaign for 9 AM; public site must stay previous version until then.

Expected outcome: Before 9 AM public shows old content; at 9 AM new content live with version bump.

Why UI-only automation breaks: CMS shows scheduled but public CDN already serves new draft—early launch.

  1. Arrange: Seed published v1 and scheduled v2 with publishAt 9 AM.
  2. Act: clock before 9—public v1; trigger jobs at 9—public v2.
  3. Assert: Probe content_state and public body hash matches v2 only after 9; v1 before.

TestChimp workflow: Instrument cms_publish with content_state; evolve scheduled path if prod scheduled share high.

Same Arrange/Act/Assert pattern as expired-coupon checkout.

Connect scenarios to your QA workflow

Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).

External references

Frequently asked questions

How do I test scheduled publish?

Set publishAt via API, clock before time assert public 404/old version, advance clock and trigger publish job route, poll content_state published and public body updated.

How do preview URLs stay private?

Preview token route returns 200 with draft; anonymous public URL 404; expired token after clock returns 403.

How do rollbacks interact with CDN?

Revert via CMS, call test purge hook, probe public version id matches reverted revision—not just editor UI.

How do approver RBAC tests work?

Writer attempts publish, probe remains draft and API 403; approver publishes, probe published.

Rich text XSS before publish?

Paste malicious HTML in editor, publish attempt should fail sanitizer probe or strip before public—see rich text pattern guide.

Multi-locale publishing?

Publish EN only, probe DE path still old locale content; publish DE, probe hreflang paths independently.

Which content_state paths need TrueCoverage?

Compare prod editorial events—evolve scheduled and rollback SmartTests when undertested vs manual publish.

Apply these patterns in your repo

Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.

Start free on TestChimp · Book a demo