import { beforeEach, describe, expect, it, vi } from 'vitest'; import { resolvePageRenderData, resolvePreviewRenderData } from '#web-runtime/api/render-data'; import { createEmptySiteSettings } from '#web-runtime/data/normalize'; import type { PageDocument, SiteSettings } from '#web-runtime/types'; const { resolvePage, getSettings, getPreview } = vi.hoisted(() => ({ resolvePage: vi.fn(), getSettings: vi.fn(), getPreview: vi.fn() })); vi.mock('#web-runtime/api/web-runtime-api', () => ({ getWebRuntimeAPI: () => ({ resolvePage, getSettings, getPreview }) })); function page(overrides: Partial = {}): PageDocument { return { id: '7', status: 'published', locale: 'en', path: 'about', groupKey: 'about', layout: 'landing', title: 'About us', seo: { title: 'About us', description: '' }, breadcrumbs: [], blocks: [], publishedAt: null, updatedAt: null, ...overrides }; } function settings(locale = 'en'): SiteSettings { return { locale, header: { menus: [], ctaItems: [ { linkType: 'external', pageId: '', externalUrl: 'https://example.com', openInNewTab: false, label: 'CTA', variant: 'primary' } ] }, footer: { copyright: '© Crux', menus: [] }, legal: { privacyPath: 'privacy', termsPath: 'terms' } }; } beforeEach(() => { resolvePage.mockReset(); getSettings.mockReset(); getPreview.mockReset(); }); // resolvePageRenderData is wrapped in React cache(); outside an RSC render // react's cache has no store and calls straight through, but each test still // uses a distinct locale/path pair so results can never leak between tests. describe('resolvePageRenderData', () => { it('returns null when the backend cannot resolve the page', async () => { resolvePage.mockResolvedValue(null); expect(await resolvePageRenderData('en', 'missing')).toBeNull(); expect(getSettings).not.toHaveBeenCalled(); }); it('refuses to render a page that is not published (the unpublish gate)', async () => { resolvePage.mockResolvedValue({ page: page({ status: 'draft', path: 'draft-page' }), languageAlternatives: [] }); expect(await resolvePageRenderData('en', 'draft-page')).toBeNull(); expect(getSettings).not.toHaveBeenCalled(); }); it('trims the locale and normalizes the path before asking the backend', async () => { resolvePage.mockResolvedValue(null); await resolvePageRenderData(' en ', '/about/team/'); expect(resolvePage).toHaveBeenCalledWith({ locale: 'en', path: 'about/team' }); }); it('uses the settings bundled into the resolution without a second request', async () => { const bundled = settings(); resolvePage.mockResolvedValue({ page: page({ path: 'bundled' }), settings: bundled, languageAlternatives: [{ locale: 'de', path: 'gebuendelt' }] }); const data = await resolvePageRenderData('en', 'bundled'); expect(data?.settings).toBe(bundled); expect(data?.languageAlternatives).toEqual([{ locale: 'de', path: 'gebuendelt' }]); expect(getSettings).not.toHaveBeenCalled(); }); it('fetches settings separately when the resolution has none', async () => { const fetched = settings(); resolvePage.mockResolvedValue({ page: page({ path: 'fetched' }), languageAlternatives: [] }); getSettings.mockResolvedValue(fetched); const data = await resolvePageRenderData('en', 'fetched'); expect(getSettings).toHaveBeenCalledWith('en'); expect(data?.settings).toBe(fetched); }); it('falls back to empty settings when the backend has none at all', async () => { resolvePage.mockResolvedValue({ page: page({ path: 'bare' }), languageAlternatives: [] }); getSettings.mockResolvedValue(null); const data = await resolvePageRenderData('en', 'bare'); expect(data?.settings).toEqual(createEmptySiteSettings('en')); }); it('returns the resolved page unchanged', async () => { const resolved = page({ path: 'unchanged' }); resolvePage.mockResolvedValue({ page: resolved, settings: settings(), languageAlternatives: [] }); const data = await resolvePageRenderData('en', 'unchanged'); expect(data?.page).toBe(resolved); }); }); describe('resolvePreviewRenderData', () => { it('returns null for an empty token without calling the backend', async () => { expect(await resolvePreviewRenderData('')).toBeNull(); expect(getPreview).not.toHaveBeenCalled(); }); it('returns null when the token resolves to nothing', async () => { getPreview.mockResolvedValue(null); expect(await resolvePreviewRenderData('token-1')).toBeNull(); expect(getPreview).toHaveBeenCalledWith('token-1'); }); it('renders a draft: previews deliberately skip the published gate', async () => { getPreview.mockResolvedValue({ page: page({ status: 'draft' }), settings: settings(), languageAlternatives: [] }); const data = await resolvePreviewRenderData('token-2'); expect(data?.page.status).toBe('draft'); }); it('falls back to empty settings in the locale of the previewed page', async () => { getPreview.mockResolvedValue({ page: page({ locale: 'de', status: 'draft' }), languageAlternatives: [] }); const data = await resolvePreviewRenderData('token-3'); expect(data?.settings).toEqual(createEmptySiteSettings('de')); }); });