Add initial project files

This commit is contained in:
Igor Martyniuk
2026-09-01 13:03:41 +03:00
parent 22e111bb44
commit 74cb5b6176
1841 changed files with 242819 additions and 30 deletions
+175
View File
@@ -0,0 +1,175 @@
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'));
});
});