164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { pageMetadata } from '#web-runtime/lib/page-metadata';
|
|
import type { PageDocument, PageRenderData, SiteSettings } from '#web-runtime/types';
|
|
|
|
const settings: SiteSettings = {
|
|
locale: 'en',
|
|
header: { menus: [], ctaItems: [] },
|
|
footer: { copyright: '', menus: [] },
|
|
legal: { privacyPath: '', termsPath: '' }
|
|
};
|
|
|
|
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 — Crux', description: 'Who we are' },
|
|
breadcrumbs: [],
|
|
blocks: [],
|
|
publishedAt: null,
|
|
updatedAt: null,
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function renderData(overrides: Partial<PageRenderData> = {}): PageRenderData {
|
|
return {
|
|
page: page(),
|
|
settings,
|
|
languageAlternatives: [],
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
/** The site serves locale-prefixed paths; metadataBase turns these into absolute URLs. */
|
|
const publicPath = (locale: string, path: string) => `/${locale}${path ? `/${path}` : ''}`;
|
|
|
|
describe('title and description', () => {
|
|
it('prefers the editor SEO fields', () => {
|
|
const metadata = pageMetadata(renderData(), 'en', publicPath);
|
|
|
|
expect(metadata.title).toBe('About us — Crux');
|
|
expect(metadata.description).toBe('Who we are');
|
|
});
|
|
|
|
it('falls back to the page title, and omits an empty description entirely', () => {
|
|
const data = renderData({ page: page({ seo: { title: '', description: '' } }) });
|
|
const metadata = pageMetadata(data, 'en', publicPath);
|
|
|
|
expect(metadata.title).toBe('About us');
|
|
expect(metadata.description).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('canonical and hreflang', () => {
|
|
it('always sets a canonical for the page itself', () => {
|
|
expect(pageMetadata(renderData(), 'en', publicPath).alternates?.canonical).toBe('/en/about');
|
|
});
|
|
|
|
it('emits no language map for a page with no translations', () => {
|
|
const data = renderData({ languageAlternatives: [{ locale: 'en', path: 'about' }] });
|
|
|
|
expect(pageMetadata(data, 'en', publicPath).alternates?.languages).toBeUndefined();
|
|
});
|
|
|
|
it('lists every locale including this one, plus x-default', () => {
|
|
const data = renderData({
|
|
languageAlternatives: [
|
|
{ locale: 'de', path: 'ueber-uns' },
|
|
{ locale: 'en', path: 'about' },
|
|
{ locale: 'uk', path: 'pro-nas' }
|
|
]
|
|
});
|
|
|
|
expect(pageMetadata(data, 'en', publicPath).alternates?.languages).toEqual({
|
|
'de': '/de/ueber-uns',
|
|
'en': '/en/about',
|
|
'uk': '/uk/pro-nas',
|
|
'x-default': '/en/about'
|
|
});
|
|
});
|
|
|
|
it('points x-default at this page when the default locale has no translation', () => {
|
|
const data = renderData({
|
|
page: page({ locale: 'uk', path: 'pro-nas' }),
|
|
languageAlternatives: [
|
|
{ locale: 'uk', path: 'pro-nas' },
|
|
{ locale: 'de', path: 'ueber-uns' }
|
|
]
|
|
});
|
|
|
|
expect(pageMetadata(data, 'en', publicPath).alternates?.languages).toMatchObject({
|
|
'x-default': '/uk/pro-nas'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('share cards', () => {
|
|
const media = {
|
|
url: 'https://cdn.example.com/hero.jpg',
|
|
alt: 'Team photo',
|
|
width: 800,
|
|
height: 525
|
|
};
|
|
|
|
it('uses the hero image, with the dimensions crawlers lay out against', () => {
|
|
const data = renderData({ page: page({ hero: { type: 'middle', ctaItems: [], media } }) });
|
|
const metadata = pageMetadata(data, 'en', publicPath);
|
|
|
|
expect(metadata.openGraph?.images).toEqual([
|
|
{ url: media.url, alt: 'Team photo', width: 800, height: 525 }
|
|
]);
|
|
expect(metadata.twitter).toMatchObject({ card: 'summary_large_image' });
|
|
});
|
|
|
|
it('falls back to the page title when the image has no alt text', () => {
|
|
const data = renderData({
|
|
page: page({ hero: { type: 'middle', ctaItems: [], media: { ...media, alt: '' } } })
|
|
});
|
|
|
|
expect(pageMetadata(data, 'en', publicPath).openGraph?.images).toMatchObject([
|
|
{ alt: 'About us' }
|
|
]);
|
|
});
|
|
|
|
it('omits dimensions the backend did not provide rather than guessing', () => {
|
|
const data = renderData({
|
|
page: page({ hero: { type: 'middle', ctaItems: [], media: { url: media.url, alt: 'x' } } })
|
|
});
|
|
|
|
expect(pageMetadata(data, 'en', publicPath).openGraph?.images).toEqual([
|
|
{ url: media.url, alt: 'x' }
|
|
]);
|
|
});
|
|
|
|
it('drops to a small card when the page has no hero image', () => {
|
|
const metadata = pageMetadata(renderData(), 'en', publicPath);
|
|
|
|
expect(metadata.openGraph?.images).toBeUndefined();
|
|
expect(metadata.twitter).toMatchObject({ card: 'summary' });
|
|
});
|
|
});
|
|
|
|
describe('open graph type', () => {
|
|
it('marks an editorial page as an article and everything else as a page', () => {
|
|
const article = renderData({ page: page({ layout: 'blog-item' }) });
|
|
const landing = renderData({ page: page({ layout: 'landing' }) });
|
|
|
|
expect(pageMetadata(article, 'en', publicPath).openGraph).toMatchObject({ type: 'article' });
|
|
expect(pageMetadata(landing, 'en', publicPath).openGraph).toMatchObject({ type: 'website' });
|
|
});
|
|
|
|
it('carries the page locale and its own url', () => {
|
|
expect(pageMetadata(renderData(), 'en', publicPath).openGraph).toMatchObject({
|
|
locale: 'en',
|
|
url: '/en/about'
|
|
});
|
|
});
|
|
});
|