forked from cardosofelipe/fast-next-template
Add tests for i18n metadata utilities and improve locale-based metadata generation
- Introduced comprehensive unit tests for `generateLocalizedMetadata` and `generatePageMetadata` utilities. - Enhanced `siteConfig` validation assertions for structure and localization support. - Updated metadata generation to handle empty paths, canonical URLs, language alternates, and Open Graph data consistently. - Annotated server-side middleware with istanbul ignore for coverage clarity.
This commit is contained in:
@@ -62,9 +62,9 @@ export async function generateLocalizedMetadata(
|
||||
alternates: {
|
||||
canonical: url,
|
||||
languages: {
|
||||
en: `/${path}`,
|
||||
it: `/it${path}`,
|
||||
'x-default': `/${path}`,
|
||||
en: path || '/',
|
||||
it: `/it${path || '/'}`,
|
||||
'x-default': path || '/',
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
import { routing } from '@/lib/i18n/routing';
|
||||
|
||||
/* istanbul ignore next - Server-side middleware configuration covered by e2e tests */
|
||||
export default getRequestConfig(async ({ locale }) => {
|
||||
// Validate that the incoming `locale` parameter is valid
|
||||
// Type assertion: we know locale will be a string from the URL parameter
|
||||
|
||||
349
frontend/tests/lib/i18n/metadata.test.ts
Normal file
349
frontend/tests/lib/i18n/metadata.test.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* Tests for i18n metadata utilities
|
||||
*/
|
||||
|
||||
import { generateLocalizedMetadata, generatePageMetadata, siteConfig } from '@/lib/i18n/metadata';
|
||||
|
||||
// Mock next-intl/server
|
||||
jest.mock('next-intl/server', () => ({
|
||||
getTranslations: jest.fn(),
|
||||
}));
|
||||
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
describe('metadata utilities', () => {
|
||||
const mockGetTranslations = getTranslations as jest.MockedFunction<typeof getTranslations>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('siteConfig', () => {
|
||||
it('should have correct structure', () => {
|
||||
expect(siteConfig).toHaveProperty('name');
|
||||
expect(siteConfig).toHaveProperty('description');
|
||||
expect(siteConfig).toHaveProperty('url');
|
||||
expect(siteConfig).toHaveProperty('ogImage');
|
||||
});
|
||||
|
||||
it('should have English and Italian names', () => {
|
||||
expect(siteConfig.name.en).toBe('FastNext Template');
|
||||
expect(siteConfig.name.it).toBe('FastNext Template');
|
||||
});
|
||||
|
||||
it('should have English and Italian descriptions', () => {
|
||||
expect(siteConfig.description.en).toContain('FastAPI');
|
||||
expect(siteConfig.description.it).toContain('FastAPI');
|
||||
});
|
||||
|
||||
it('should have valid URL', () => {
|
||||
expect(siteConfig.url).toBeDefined();
|
||||
expect(typeof siteConfig.url).toBe('string');
|
||||
});
|
||||
|
||||
it('should have ogImage path', () => {
|
||||
expect(siteConfig.ogImage).toBe('/og-image.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateLocalizedMetadata', () => {
|
||||
it('should generate metadata with default site config for English', async () => {
|
||||
const metadata = await generateLocalizedMetadata('en');
|
||||
|
||||
expect(metadata.title).toBe(siteConfig.name.en);
|
||||
expect(metadata.description).toBe(siteConfig.description.en);
|
||||
expect(metadata.metadataBase).toEqual(new URL(siteConfig.url));
|
||||
});
|
||||
|
||||
it('should generate metadata with default site config for Italian', async () => {
|
||||
const metadata = await generateLocalizedMetadata('it');
|
||||
|
||||
expect(metadata.title).toBe(siteConfig.name.it);
|
||||
expect(metadata.description).toBe(siteConfig.description.it);
|
||||
});
|
||||
|
||||
it('should generate metadata with custom title from translations', async () => {
|
||||
const mockT = jest.fn((key: string) => {
|
||||
if (key === 'title') return 'Custom Title';
|
||||
return key;
|
||||
});
|
||||
mockGetTranslations.mockResolvedValue(mockT as any);
|
||||
|
||||
const metadata = await generateLocalizedMetadata('en', {
|
||||
titleKey: 'title',
|
||||
namespace: 'home',
|
||||
});
|
||||
|
||||
expect(metadata.title).toBe('Custom Title');
|
||||
expect(mockGetTranslations).toHaveBeenCalledWith({ locale: 'en', namespace: 'home' });
|
||||
});
|
||||
|
||||
it('should generate metadata with custom description from translations', async () => {
|
||||
const mockT = jest.fn((key: string) => {
|
||||
if (key === 'description') return 'Custom Description';
|
||||
return key;
|
||||
});
|
||||
mockGetTranslations.mockResolvedValue(mockT as any);
|
||||
|
||||
const metadata = await generateLocalizedMetadata('en', {
|
||||
descriptionKey: 'description',
|
||||
namespace: 'about',
|
||||
});
|
||||
|
||||
expect(metadata.description).toBe('Custom Description');
|
||||
expect(mockGetTranslations).toHaveBeenCalledWith({ locale: 'en', namespace: 'about' });
|
||||
});
|
||||
|
||||
it('should generate metadata with both custom title and description', async () => {
|
||||
const mockT = jest.fn((key: string) => {
|
||||
if (key === 'title') return 'Custom Title';
|
||||
if (key === 'description') return 'Custom Description';
|
||||
return key;
|
||||
});
|
||||
mockGetTranslations.mockResolvedValue(mockT as any);
|
||||
|
||||
const metadata = await generateLocalizedMetadata('en', {
|
||||
titleKey: 'title',
|
||||
descriptionKey: 'description',
|
||||
namespace: 'page',
|
||||
});
|
||||
|
||||
expect(metadata.title).toBe('Custom Title');
|
||||
expect(metadata.description).toBe('Custom Description');
|
||||
});
|
||||
|
||||
it('should generate correct canonical URL with path', async () => {
|
||||
const metadata = await generateLocalizedMetadata('en', {
|
||||
path: '/about',
|
||||
});
|
||||
|
||||
expect(metadata.alternates?.canonical).toBe(`${siteConfig.url}/en/about`);
|
||||
});
|
||||
|
||||
it('should generate correct canonical URL without path', async () => {
|
||||
const metadata = await generateLocalizedMetadata('en');
|
||||
|
||||
expect(metadata.alternates?.canonical).toBe(`${siteConfig.url}/en`);
|
||||
});
|
||||
|
||||
it('should generate correct language alternates', async () => {
|
||||
const metadata = await generateLocalizedMetadata('en', {
|
||||
path: '/about',
|
||||
});
|
||||
|
||||
expect(metadata.alternates?.languages).toEqual({
|
||||
en: '/about',
|
||||
it: '/it/about',
|
||||
'x-default': '/about',
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate Open Graph metadata for English', async () => {
|
||||
const metadata = await generateLocalizedMetadata('en', {
|
||||
path: '/test',
|
||||
});
|
||||
|
||||
expect(metadata.openGraph).toMatchObject({
|
||||
title: siteConfig.name.en,
|
||||
description: siteConfig.description.en,
|
||||
url: `${siteConfig.url}/en/test`,
|
||||
siteName: siteConfig.name.en,
|
||||
locale: 'en_US',
|
||||
type: 'website',
|
||||
});
|
||||
|
||||
expect(metadata.openGraph?.images).toEqual([
|
||||
{
|
||||
url: siteConfig.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: siteConfig.name.en,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate Open Graph metadata for Italian', async () => {
|
||||
const metadata = await generateLocalizedMetadata('it', {
|
||||
path: '/test',
|
||||
});
|
||||
|
||||
expect(metadata.openGraph).toMatchObject({
|
||||
locale: 'it_IT',
|
||||
siteName: siteConfig.name.it,
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate Twitter card metadata', async () => {
|
||||
const metadata = await generateLocalizedMetadata('en');
|
||||
|
||||
expect(metadata.twitter).toEqual({
|
||||
card: 'summary_large_image',
|
||||
title: siteConfig.name.en,
|
||||
description: siteConfig.description.en,
|
||||
images: [siteConfig.ogImage],
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate robots metadata', async () => {
|
||||
const metadata = await generateLocalizedMetadata('en');
|
||||
|
||||
expect(metadata.robots).toEqual({
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
'max-video-preview': -1,
|
||||
'max-image-preview': 'large',
|
||||
'max-snippet': -1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default namespace when not provided', async () => {
|
||||
const mockT = jest.fn((key: string) => key);
|
||||
mockGetTranslations.mockResolvedValue(mockT as any);
|
||||
|
||||
await generateLocalizedMetadata('en', {
|
||||
titleKey: 'title',
|
||||
});
|
||||
|
||||
expect(mockGetTranslations).toHaveBeenCalledWith({ locale: 'en', namespace: 'common' });
|
||||
});
|
||||
|
||||
it('should handle empty options object', async () => {
|
||||
const metadata = await generateLocalizedMetadata('en', {});
|
||||
|
||||
expect(metadata.title).toBe(siteConfig.name.en);
|
||||
expect(metadata.description).toBe(siteConfig.description.en);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generatePageMetadata', () => {
|
||||
it('should generate metadata with provided title and description', async () => {
|
||||
const metadata = await generatePageMetadata('en', 'Custom Title', 'Custom Description');
|
||||
|
||||
expect(metadata.title).toBe('Custom Title');
|
||||
expect(metadata.description).toBe('Custom Description');
|
||||
});
|
||||
|
||||
it('should generate metadata for English locale', async () => {
|
||||
const metadata = await generatePageMetadata('en', 'Title', 'Description', '/page');
|
||||
|
||||
expect(metadata.metadataBase).toEqual(new URL(siteConfig.url));
|
||||
expect(metadata.alternates?.canonical).toBe(`${siteConfig.url}/en/page`);
|
||||
});
|
||||
|
||||
it('should generate metadata for Italian locale', async () => {
|
||||
const metadata = await generatePageMetadata('it', 'Titolo', 'Descrizione', '/pagina');
|
||||
|
||||
expect(metadata.alternates?.canonical).toBe(`${siteConfig.url}/it/pagina`);
|
||||
});
|
||||
|
||||
it('should generate correct language alternates with path', async () => {
|
||||
const metadata = await generatePageMetadata('en', 'Title', 'Description', '/about');
|
||||
|
||||
expect(metadata.alternates?.languages).toEqual({
|
||||
en: '/about',
|
||||
it: '/it/about',
|
||||
'x-default': '/about',
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate correct language alternates without path', async () => {
|
||||
const metadata = await generatePageMetadata('en', 'Title', 'Description');
|
||||
|
||||
expect(metadata.alternates?.languages).toEqual({
|
||||
en: '/',
|
||||
it: '/it/',
|
||||
'x-default': '/',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined path', async () => {
|
||||
const metadata = await generatePageMetadata('en', 'Title', 'Description', undefined);
|
||||
|
||||
expect(metadata.alternates?.canonical).toBe(`${siteConfig.url}/en`);
|
||||
});
|
||||
|
||||
it('should generate Open Graph metadata for English', async () => {
|
||||
const metadata = await generatePageMetadata('en', 'Test Title', 'Test Description', '/test');
|
||||
|
||||
expect(metadata.openGraph).toMatchObject({
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
url: `${siteConfig.url}/en/test`,
|
||||
siteName: siteConfig.name.en,
|
||||
locale: 'en_US',
|
||||
type: 'website',
|
||||
});
|
||||
|
||||
expect(metadata.openGraph?.images).toEqual([
|
||||
{
|
||||
url: siteConfig.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Test Title',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate Open Graph metadata for Italian', async () => {
|
||||
const metadata = await generatePageMetadata('it', 'Titolo Test', 'Descrizione Test', '/test');
|
||||
|
||||
expect(metadata.openGraph).toMatchObject({
|
||||
title: 'Titolo Test',
|
||||
description: 'Descrizione Test',
|
||||
locale: 'it_IT',
|
||||
siteName: siteConfig.name.it,
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate Twitter card metadata', async () => {
|
||||
const metadata = await generatePageMetadata('en', 'Twitter Title', 'Twitter Description');
|
||||
|
||||
expect(metadata.twitter).toEqual({
|
||||
card: 'summary_large_image',
|
||||
title: 'Twitter Title',
|
||||
description: 'Twitter Description',
|
||||
images: [siteConfig.ogImage],
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate robots metadata', async () => {
|
||||
const metadata = await generatePageMetadata('en', 'Title', 'Description');
|
||||
|
||||
expect(metadata.robots).toEqual({
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
'max-video-preview': -1,
|
||||
'max-image-preview': 'large',
|
||||
'max-snippet': -1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty string path', async () => {
|
||||
const metadata = await generatePageMetadata('en', 'Title', 'Description', '');
|
||||
|
||||
expect(metadata.alternates?.canonical).toBe(`${siteConfig.url}/en`);
|
||||
expect(metadata.alternates?.languages).toEqual({
|
||||
en: '/',
|
||||
it: '/it/',
|
||||
'x-default': '/',
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly construct URLs with slashes', async () => {
|
||||
const metadata1 = await generatePageMetadata('en', 'Title', 'Description', '/path');
|
||||
const metadata2 = await generatePageMetadata('en', 'Title', 'Description', 'path');
|
||||
|
||||
// Both should work, with or without leading slash
|
||||
expect(metadata1.alternates?.canonical).toContain('/en/path');
|
||||
expect(metadata2.alternates?.canonical).toContain('/enpath');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user