176 lines
5.4 KiB
TypeScript
176 lines
5.4 KiB
TypeScript
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> = {}): 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'));
|
|
});
|
|
});
|