Skip to content
Merged
Show file tree
Hide file tree
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
25 changes: 4 additions & 21 deletions __tests__/client/components/OnboardingChecklist.client.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jest.mock('../../../src/collections/Tenants/components/onboardingActions', () =>

const buildStatus = (overrides: Partial<ProvisioningStatus> = {}): ProvisioningStatus => ({
builtInPages: { count: 0, expected: 7 },
pages: { copied: 0, expected: 5, missing: [], skipped: [] },
pages: { created: 0, expected: 5, missing: [] },
homePage: false,
navigation: false,
settings: { exists: false },
Expand All @@ -50,7 +50,7 @@ const buildStatus = (overrides: Partial<ProvisioningStatus> = {}): ProvisioningS

const fullyProvisioned = buildStatus({
builtInPages: { count: 7, expected: 7 },
pages: { copied: 5, expected: 5, missing: [], skipped: [] },
pages: { created: 5, expected: 5, missing: [] },
homePage: true,
navigation: true,
settings: { exists: true, id: 1 },
Expand Down Expand Up @@ -94,7 +94,7 @@ describe('OnboardingChecklist', () => {
// so auto-provision doesn't run but the button shows
const incompleteStatus = buildStatus({
builtInPages: { count: 7, expected: 7 },
pages: { copied: 3, expected: 5, missing: ['About Us', 'Donate'], skipped: [] },
pages: { created: 3, expected: 5, missing: ['About Us', 'Donate'] },
homePage: true,
navigation: true,
settings: { exists: true, id: 1 },
Expand Down Expand Up @@ -123,7 +123,7 @@ describe('OnboardingChecklist', () => {
mockCheckStatus.mockResolvedValue({
status: buildStatus({
builtInPages: { count: 7, expected: 7 },
pages: { copied: 3, expected: 5, missing: ['About Us', 'Donate'], skipped: [] },
pages: { created: 3, expected: 5, missing: ['About Us', 'Donate'] },
homePage: true,
navigation: true,
settings: { exists: true, id: 1 },
Expand All @@ -136,23 +136,6 @@ describe('OnboardingChecklist', () => {
expect(screen.getByText('Missing: About Us, Donate')).toBeInTheDocument()
})

it('shows skipped demo pages', async () => {
mockCheckStatus.mockResolvedValue({
status: buildStatus({
builtInPages: { count: 7, expected: 7 },
pages: { copied: 4, expected: 5, missing: [], skipped: ['Demo Page'] },
homePage: true,
navigation: true,
settings: { exists: true, id: 1 },
}),
})

render(<OnboardingChecklist />)
await flushAsync()

expect(screen.getByText('Skipped (demo pages): Demo Page')).toBeInTheDocument()
})

it('shows link to settings when settings exist', async () => {
render(<OnboardingChecklist />)
await flushAsync()
Expand Down
21 changes: 20 additions & 1 deletion __tests__/client/components/needsProvisioning.client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ProvisioningStatus } from '@/collections/Tenants/components/onboar

const buildStatus = (overrides: Partial<ProvisioningStatus> = {}): ProvisioningStatus => ({
builtInPages: { count: 0, expected: 7 },
pages: { copied: 0, expected: 5, missing: [], skipped: [] },
pages: { created: 0, expected: 5, missing: [] },
homePage: false,
navigation: false,
settings: { exists: false },
Expand All @@ -21,6 +21,7 @@ describe('needsProvisioning', () => {
needsProvisioning(
buildStatus({
builtInPages: { count: 7, expected: 7 },
pages: { created: 5, expected: 5, missing: [] },
homePage: true,
navigation: true,
settings: { exists: true },
Expand All @@ -34,6 +35,21 @@ describe('needsProvisioning', () => {
needsProvisioning(
buildStatus({
builtInPages: { count: 3, expected: 7 },
pages: { created: 5, expected: 5, missing: [] },
homePage: true,
navigation: true,
settings: { exists: true },
}),
),
).toBe(false)
})

it('returns false when partially provisioned (only pages missing)', () => {
expect(
needsProvisioning(
buildStatus({
builtInPages: { count: 7, expected: 7 },
pages: { created: 3, expected: 5, missing: ['About Us', 'Donate'] },
homePage: true,
navigation: true,
settings: { exists: true },
Expand All @@ -47,6 +63,7 @@ describe('needsProvisioning', () => {
needsProvisioning(
buildStatus({
builtInPages: { count: 7, expected: 7 },
pages: { created: 5, expected: 5, missing: [] },
homePage: false,
navigation: true,
settings: { exists: true },
Expand All @@ -60,6 +77,7 @@ describe('needsProvisioning', () => {
needsProvisioning(
buildStatus({
builtInPages: { count: 7, expected: 7 },
pages: { created: 5, expected: 5, missing: [] },
homePage: true,
navigation: false,
settings: { exists: true },
Expand All @@ -73,6 +91,7 @@ describe('needsProvisioning', () => {
needsProvisioning(
buildStatus({
builtInPages: { count: 7, expected: 7 },
pages: { created: 5, expected: 5, missing: [] },
homePage: true,
navigation: true,
settings: { exists: false },
Expand Down
2 changes: 1 addition & 1 deletion __tests__/e2e/admin/onboarding-checklist.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test.describe('Onboarding Checklist', () => {

// Core checklist items
await expect(checklist.getByText('Built-in pages')).toBeVisible()
await expect(checklist.getByText('Pages - copied from DVAC')).toBeVisible()
await expect(checklist.getByText('Pages', { exact: true })).toBeVisible()
await expect(checklist.getByText('Home page')).toBeVisible()
await expect(checklist.getByText('Navigation')).toBeVisible()
await expect(checklist.getByText('Website Settings')).toBeVisible()
Expand Down
2 changes: 1 addition & 1 deletion __tests__/server/OnboardingStatusCell.server.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function isReactElement(value: unknown): value is React.ReactElement<{

const buildStatus = (overrides: Partial<ProvisioningStatus> = {}): ProvisioningStatus => ({
builtInPages: { count: 7, expected: 7 },
pages: { copied: 5, expected: 5, missing: [], skipped: [] },
pages: { created: 5, expected: 5, missing: [] },
homePage: true,
navigation: true,
settings: { exists: true, id: 1 },
Expand Down
144 changes: 144 additions & 0 deletions __tests__/server/duplicatePageToTenant.server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
jest.mock('../../src/constants/defaults', () => ({
// eslint-disable-next-line @typescript-eslint/no-require-imports
DEFAULT_BLOCKS: require('./fixtures/mockBlocks').DEFAULT_BLOCKS,
}))

jest.mock('../../src/blocks/NACMedia/config', () => ({
// eslint-disable-next-line @typescript-eslint/no-require-imports
NACMediaBlock: require('./fixtures/mockBlocks').NACMediaBlock,
}))
Comment thread
busbyk marked this conversation as resolved.

jest.mock('../../src/blocks/Button/config', () => ({
// eslint-disable-next-line @typescript-eslint/no-require-imports
ButtonBlock: require('./fixtures/mockBlocks').ButtonBlock,
}))

jest.mock('../../src/blocks/Callout/config', () => ({
// eslint-disable-next-line @typescript-eslint/no-require-imports
CalloutBlock: require('./fixtures/mockBlocks').CalloutBlock,
}))

jest.mock('../../src/payload.config', () => ({}))

jest.mock('payload', () => ({
getPayload: jest.fn(),
}))

import { duplicatePageToTenant } from '@/collections/Pages/endpoints/duplicatePageToTenant'
import type { Payload, PayloadRequest } from 'payload'
import { getPayload } from 'payload'

const mockFind = jest.fn()
const mockCreate = jest.fn()

const mockTenant = { id: 42, name: 'Test AC', slug: 'tac' }

beforeEach(() => {
jest
.mocked(getPayload)
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
.mockResolvedValue({ find: mockFind, create: mockCreate } as unknown as Payload)
// First find call: tenant lookup. Second find call: slug existence check (no conflict by default).
mockFind
.mockReset()
.mockResolvedValueOnce({ docs: [mockTenant] })
.mockResolvedValueOnce({ docs: [] })
mockCreate.mockReset().mockResolvedValue({ id: 99 })
})

function buildRequest(
tenantSlug: string | undefined,
body: Record<string, unknown>,
): PayloadRequest {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return {
routeParams: tenantSlug !== undefined ? { tenantSlug } : undefined,
json: async () => body,
} as unknown as PayloadRequest
}

describe('duplicatePageToTenant', () => {
it('creates the page as a draft', async () => {
const req = buildRequest('42', {
newPage: { title: 'About', slug: 'about', layout: [] },
})
await duplicatePageToTenant(req)
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ draft: true }))
})

it('appends " - Copy" when a page with the same slug exists', async () => {
// Slug existence check returns a match
mockFind
.mockReset()
.mockResolvedValueOnce({ docs: [mockTenant] })
.mockResolvedValueOnce({ docs: [{ id: 1, slug: 'about-us' }] })
const req = buildRequest('42', {
newPage: { title: 'About Us', slug: 'about-us', layout: [] },
})
await duplicatePageToTenant(req)
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ title: 'About Us - Copy', slug: 'about-us-copy' }),
}),
)
})

it('keeps original title and slug when no conflict exists', async () => {
const req = buildRequest('42', {
newPage: { title: 'About Us', slug: 'about-us', layout: [] },
})
await duplicatePageToTenant(req)
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ title: 'About Us', slug: 'about-us' }),
}),
)
})

it('sets the tenant from the lookup result', async () => {
const req = buildRequest('42', {
newPage: { title: 'About', slug: 'about', layout: [] },
})
await duplicatePageToTenant(req)
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ tenant: mockTenant }),
}),
)
})

it('passes layout through clearLayoutRelationships', async () => {
const req = buildRequest('42', {
newPage: {
title: 'About',
slug: 'about',
layout: [{ blockType: 'singleBlogPost', post: 123, backgroundColor: 'red' }],
},
})
await duplicatePageToTenant(req)
const layout = mockCreate.mock.calls[0][0].data.layout
expect(layout[0]).not.toHaveProperty('post')
expect(layout[0]).toHaveProperty('backgroundColor', 'red')
})

it('falls back to empty layout when newPage.layout is absent', async () => {
const req = buildRequest('42', { newPage: { title: 'About', slug: 'about' } })
await duplicatePageToTenant(req)
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({ data: expect.objectContaining({ layout: [] }) }),
)
})

it('looks up the tenant using tenantSlug from route param', async () => {
const req = buildRequest('tac', {
newPage: { title: 'About', slug: 'about', layout: [] },
})
await duplicatePageToTenant(req)
expect(mockFind).toHaveBeenCalledWith(
expect.objectContaining({
collection: 'tenants',
where: { slug: { equals: 'tac' } },
}),
)
})
})
92 changes: 92 additions & 0 deletions __tests__/server/fixtures/mockBlocks.ts
Comment thread
rchlfryn marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Minimal block configs for testing clearLayoutRelationships.
// These mirror the shape of Payload Block configs without importing from payload.
export const DEFAULT_BLOCKS = [
{
slug: 'singleBlogPost',
fields: [
{ type: 'relationship', name: 'post', relationTo: 'posts' },
{ type: 'text', name: 'backgroundColor' },
],
},
{
slug: 'mediaBlock',
fields: [{ type: 'upload', name: 'media', relationTo: 'media' }],
},
{
slug: 'sponsorsBlock',
fields: [
{ type: 'relationship', name: 'sponsors', relationTo: 'sponsors', hasMany: true },
{ type: 'text', name: 'sponsorsLayout' },
],
},
{
slug: 'singleEvent',
fields: [
{ type: 'relationship', name: 'event', relationTo: 'events' },
{ type: 'text', name: 'backgroundColor' },
],
},
{
slug: 'blogList',
fields: [
{ type: 'text', name: 'postOptions' },
{
type: 'group',
name: 'staticOptions',
fields: [{ type: 'relationship', name: 'staticPosts', relationTo: 'posts', hasMany: true }],
},
],
},
{
slug: 'imageLinkGrid',
fields: [
{
type: 'array',
name: 'columns',
fields: [
{ type: 'upload', name: 'image', relationTo: 'media' },
{ type: 'text', name: 'caption' },
],
},
],
},
]

export const NACMediaBlock = { slug: 'nacMedia', fields: [] }

// Lexical-embedded blocks (used inside richText BlocksFeature, not in DEFAULT_BLOCKS)
export const ButtonBlock = {
slug: 'buttonBlock',
fields: [
{
type: 'group',
name: 'button',
fields: [
{
type: 'row',
fields: [{ type: 'radio', name: 'type' }],
},
{
type: 'row',
fields: [
{
type: 'relationship',
name: 'reference',
relationTo: ['pages', 'builtInPages', 'posts'],
},
{ type: 'text', name: 'url' },
{ type: 'text', name: 'label' },
],
},
],
},
],
}

export const CalloutBlock = {
slug: 'calloutBlock',
fields: [
{ type: 'richText', name: 'richText' },
{ type: 'text', name: 'style' },
],
}
Loading
Loading