test: add comprehensive test suite for components, composables, and pages
Add 16 new test files covering: - Composables: useBackend, useMeta, useDataJson - Type classes: QueryResult, ResumeContent - UI components: BadgeList, BadgeListCard - Navbar components: LanguageSwitcher, ThemeSwitcher - App components: AppNavbar, AppFooter - VocalSynth components: Projects, Tools - Pages: contact, resume, [...slug] Tests focus on pure logic, interfaces, and component rendering where possible, avoiding complex mocking of Nuxt auto-imported composables. Total: 174 tests across 17 test files (including existing useApi tests).
This commit is contained in:
97
app/composables/useBackend.test.ts
Normal file
97
app/composables/useBackend.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { useBackend } from './useBackend';
|
||||
import type { MetaResponse } from '~/types/api/meta';
|
||||
import type { ContactResponse } from '~/types/api/contact';
|
||||
|
||||
// Mock useApi
|
||||
const mockGet = vi.fn();
|
||||
const mockPost = vi.fn();
|
||||
|
||||
vi.mock('./useApi', () => ({
|
||||
useApi: vi.fn(() => ({
|
||||
get: mockGet,
|
||||
post: mockPost,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('useBackend', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getMeta', () => {
|
||||
it('should call useApi.get with /meta endpoint', () => {
|
||||
const mockResult = {
|
||||
data: ref<MetaResponse | null>({ version: '1.0.0', name: 'Test' }),
|
||||
error: ref(null),
|
||||
loading: ref(false),
|
||||
run: vi.fn(),
|
||||
};
|
||||
mockGet.mockReturnValue(mockResult);
|
||||
|
||||
const { getMeta } = useBackend();
|
||||
const result = getMeta();
|
||||
|
||||
expect(mockGet).toHaveBeenCalledWith('/meta');
|
||||
expect(result).toBe(mockResult);
|
||||
});
|
||||
|
||||
it('should return UseApiResponse with correct structure', () => {
|
||||
const mockResult = {
|
||||
data: ref<MetaResponse | null>(null),
|
||||
error: ref(null),
|
||||
loading: ref(false),
|
||||
run: vi.fn(),
|
||||
};
|
||||
mockGet.mockReturnValue(mockResult);
|
||||
|
||||
const { getMeta } = useBackend();
|
||||
const result = getMeta();
|
||||
|
||||
expect(result).toHaveProperty('data');
|
||||
expect(result).toHaveProperty('error');
|
||||
expect(result).toHaveProperty('loading');
|
||||
expect(result).toHaveProperty('run');
|
||||
});
|
||||
});
|
||||
|
||||
describe('postContact', () => {
|
||||
it('should call useApi.post with /contact endpoint and immediate=false', () => {
|
||||
const mockResult = {
|
||||
data: ref<ContactResponse | null>(null),
|
||||
error: ref(null),
|
||||
loading: ref(false),
|
||||
run: vi.fn(),
|
||||
};
|
||||
mockPost.mockReturnValue(mockResult);
|
||||
|
||||
const { postContact } = useBackend();
|
||||
const result = postContact();
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith('/contact', undefined, false);
|
||||
expect(result).toBe(mockResult);
|
||||
});
|
||||
|
||||
it('should return UseApiResponse with correct structure', () => {
|
||||
const mockResult = {
|
||||
data: ref<ContactResponse | null>(null),
|
||||
error: ref(null),
|
||||
loading: ref(false),
|
||||
run: vi.fn(),
|
||||
};
|
||||
mockPost.mockReturnValue(mockResult);
|
||||
|
||||
const { postContact } = useBackend();
|
||||
const result = postContact();
|
||||
|
||||
expect(result).toHaveProperty('data');
|
||||
expect(result).toHaveProperty('error');
|
||||
expect(result).toHaveProperty('loading');
|
||||
expect(result).toHaveProperty('run');
|
||||
});
|
||||
});
|
||||
});
|
||||
187
app/composables/useDataJson.test.ts
Normal file
187
app/composables/useDataJson.test.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { withLeadingSlash } from 'ufo';
|
||||
|
||||
describe('useDataJson', () => {
|
||||
describe('withLeadingSlash utility', () => {
|
||||
it('should add leading slash to path without one', () => {
|
||||
expect(withLeadingSlash('test-page')).toBe('/test-page');
|
||||
});
|
||||
|
||||
it('should preserve leading slash if already present', () => {
|
||||
expect(withLeadingSlash('/test-page')).toBe('/test-page');
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(withLeadingSlash('')).toBe('/');
|
||||
});
|
||||
|
||||
it('should handle complex paths', () => {
|
||||
expect(withLeadingSlash('vocal-synthesis/keine-tashi')).toBe('/vocal-synthesis/keine-tashi');
|
||||
});
|
||||
});
|
||||
|
||||
describe('slug computation logic', () => {
|
||||
it('should convert array slug to string with leading slash', () => {
|
||||
const slugParam = ['vocal-synthesis', 'keine-tashi'];
|
||||
const slug = withLeadingSlash(String(slugParam));
|
||||
expect(slug).toBe('/vocal-synthesis,keine-tashi');
|
||||
});
|
||||
|
||||
it('should use route path as fallback when no slug', () => {
|
||||
const slugParam = '';
|
||||
const routePath = '/fallback-path';
|
||||
const slug = withLeadingSlash(String(slugParam || routePath));
|
||||
expect(slug).toBe('/fallback-path');
|
||||
});
|
||||
|
||||
it('should prefer slug param over route path', () => {
|
||||
const slugParam = 'my-page';
|
||||
const routePath = '/different-path';
|
||||
const slug = withLeadingSlash(String(slugParam || routePath));
|
||||
expect(slug).toBe('/my-page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('key computation logic', () => {
|
||||
it('should create cache key from prefix and slug', () => {
|
||||
const prefix = 'page';
|
||||
const slug = '/test-page';
|
||||
const key = prefix + '-' + slug;
|
||||
expect(key).toBe('page-/test-page');
|
||||
});
|
||||
|
||||
it('should create unique keys for different prefixes', () => {
|
||||
const slug = '/resume';
|
||||
const pageKey = 'page' + '-' + slug;
|
||||
const dataKey = 'page-data' + '-' + slug;
|
||||
expect(pageKey).not.toBe(dataKey);
|
||||
expect(pageKey).toBe('page-/resume');
|
||||
expect(dataKey).toBe('page-data-/resume');
|
||||
});
|
||||
});
|
||||
|
||||
describe('collection name construction', () => {
|
||||
it('should construct collection name from prefix and locale', () => {
|
||||
const collectionPrefix = 'content_';
|
||||
const locale = 'en';
|
||||
const collection = collectionPrefix + locale;
|
||||
expect(collection).toBe('content_en');
|
||||
});
|
||||
|
||||
it('should handle French locale', () => {
|
||||
const collectionPrefix = 'content_';
|
||||
const locale = 'fr';
|
||||
const collection = collectionPrefix + locale;
|
||||
expect(collection).toBe('content_fr');
|
||||
});
|
||||
|
||||
it('should handle data collection prefix', () => {
|
||||
const collectionPrefix = 'content_data_';
|
||||
const locale = 'en';
|
||||
const collection = collectionPrefix + locale;
|
||||
expect(collection).toBe('content_data_en');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getData options structure', () => {
|
||||
it('should support useFilter option', () => {
|
||||
const options = { useFilter: true };
|
||||
expect(options.useFilter).toBe(true);
|
||||
});
|
||||
|
||||
it('should support fallbackToEnglish option', () => {
|
||||
const options = { fallbackToEnglish: true };
|
||||
expect(options.fallbackToEnglish).toBe(true);
|
||||
});
|
||||
|
||||
it('should support extractMeta option', () => {
|
||||
const options = { extractMeta: true };
|
||||
expect(options.extractMeta).toBe(true);
|
||||
});
|
||||
|
||||
it('should have sensible defaults', () => {
|
||||
const options = {
|
||||
useFilter: false,
|
||||
fallbackToEnglish: false,
|
||||
extractMeta: false,
|
||||
};
|
||||
expect(options.useFilter).toBe(false);
|
||||
expect(options.fallbackToEnglish).toBe(false);
|
||||
expect(options.extractMeta).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getJsonData configuration', () => {
|
||||
it('should use useFilter=true for data collections', () => {
|
||||
// getJsonData calls getData with useFilter=true
|
||||
const expectedOptions = { useFilter: true, extractMeta: true };
|
||||
expect(expectedOptions.useFilter).toBe(true);
|
||||
expect(expectedOptions.extractMeta).toBe(true);
|
||||
});
|
||||
|
||||
it('should have default collection prefix', () => {
|
||||
const defaultPrefix = 'content_data_';
|
||||
expect(defaultPrefix).toBe('content_data_');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPageContent configuration', () => {
|
||||
it('should use fallbackToEnglish by default', () => {
|
||||
const defaultFallback = true;
|
||||
expect(defaultFallback).toBe(true);
|
||||
});
|
||||
|
||||
it('should have default collection prefix', () => {
|
||||
const defaultPrefix = 'content_';
|
||||
expect(defaultPrefix).toBe('content_');
|
||||
});
|
||||
});
|
||||
|
||||
describe('meta extraction logic', () => {
|
||||
it('should return meta when extractMeta is true', () => {
|
||||
const content = {
|
||||
body: 'some content',
|
||||
meta: { path: '/test', title: 'Test' },
|
||||
};
|
||||
const extractMeta = true;
|
||||
const result = extractMeta ? content?.meta : content;
|
||||
expect(result).toEqual({ path: '/test', title: 'Test' });
|
||||
});
|
||||
|
||||
it('should return full content when extractMeta is false', () => {
|
||||
const content = {
|
||||
body: 'some content',
|
||||
meta: { path: '/test', title: 'Test' },
|
||||
};
|
||||
const extractMeta = false;
|
||||
const result = extractMeta ? content?.meta : content;
|
||||
expect(result).toEqual(content);
|
||||
});
|
||||
|
||||
it('should handle null content gracefully', () => {
|
||||
const content = null;
|
||||
const extractMeta = true;
|
||||
const result = extractMeta ? content?.meta : content;
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter logic for data collections', () => {
|
||||
it('should filter by meta.path matching slug', () => {
|
||||
const allData = [
|
||||
{ meta: { path: '/resume' }, data: 'resume data' },
|
||||
{ meta: { path: '/other' }, data: 'other data' },
|
||||
];
|
||||
const slug = '/resume';
|
||||
const content = allData.filter((source) => source.meta.path === slug)[0];
|
||||
expect(content).toEqual({ meta: { path: '/resume' }, data: 'resume data' });
|
||||
});
|
||||
|
||||
it('should return undefined when no match found', () => {
|
||||
const allData = [{ meta: { path: '/other' }, data: 'other data' }];
|
||||
const slug = '/nonexistent';
|
||||
const content = allData.filter((source) => source.meta.path === slug)[0];
|
||||
expect(content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
101
app/composables/useMeta.test.ts
Normal file
101
app/composables/useMeta.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { MetaImageOptions, MetaOptions } from './useMeta';
|
||||
|
||||
describe('useMeta', () => {
|
||||
describe('MetaOptions interface', () => {
|
||||
it('should accept required title and description', () => {
|
||||
const options: MetaOptions = {
|
||||
title: 'Test Page',
|
||||
description: 'Test description',
|
||||
};
|
||||
|
||||
expect(options.title).toBe('Test Page');
|
||||
expect(options.description).toBe('Test description');
|
||||
expect(options.image).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should accept optional image property', () => {
|
||||
const options: MetaOptions = {
|
||||
title: 'Test Page',
|
||||
description: 'Test description',
|
||||
image: {
|
||||
url: 'https://example.com/image.jpg',
|
||||
alt: 'Alt text',
|
||||
},
|
||||
};
|
||||
|
||||
expect(options.image).toBeDefined();
|
||||
expect(options.image?.url).toBe('https://example.com/image.jpg');
|
||||
expect(options.image?.alt).toBe('Alt text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MetaImageOptions interface', () => {
|
||||
it('should require url and alt properties', () => {
|
||||
const imageOptions: MetaImageOptions = {
|
||||
url: 'https://example.com/image.png',
|
||||
alt: 'Image description',
|
||||
};
|
||||
|
||||
expect(imageOptions.url).toBe('https://example.com/image.png');
|
||||
expect(imageOptions.alt).toBe('Image description');
|
||||
});
|
||||
});
|
||||
|
||||
describe('title suffix logic', () => {
|
||||
const titleSuffix = ' – Lucien Cartier-Tilet';
|
||||
|
||||
it('should append suffix to title', () => {
|
||||
const title = 'My Page';
|
||||
const fullTitle = title + titleSuffix;
|
||||
|
||||
expect(fullTitle).toBe('My Page – Lucien Cartier-Tilet');
|
||||
});
|
||||
|
||||
it('should handle empty title', () => {
|
||||
const title = '';
|
||||
const fullTitle = title + titleSuffix;
|
||||
|
||||
expect(fullTitle).toBe(' – Lucien Cartier-Tilet');
|
||||
});
|
||||
});
|
||||
|
||||
describe('twitter card type logic', () => {
|
||||
it('should use summary_large_image when image is provided', () => {
|
||||
const image: MetaImageOptions = { url: 'test.jpg', alt: 'Test' };
|
||||
const cardType = image ? 'summary_large_image' : 'summary';
|
||||
|
||||
expect(cardType).toBe('summary_large_image');
|
||||
});
|
||||
|
||||
it('should use summary when no image is provided', () => {
|
||||
const image: MetaImageOptions | undefined = undefined;
|
||||
const cardType = image ? 'summary_large_image' : 'summary';
|
||||
|
||||
expect(cardType).toBe('summary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('optional chaining for image properties', () => {
|
||||
it('should return url when image is provided', () => {
|
||||
const options: MetaOptions = {
|
||||
title: 'Test',
|
||||
description: 'Test',
|
||||
image: { url: 'https://example.com/og.jpg', alt: 'OG Image' },
|
||||
};
|
||||
|
||||
expect(options.image?.url).toBe('https://example.com/og.jpg');
|
||||
expect(options.image?.alt).toBe('OG Image');
|
||||
});
|
||||
|
||||
it('should return undefined when image is not provided', () => {
|
||||
const options: MetaOptions = {
|
||||
title: 'Test',
|
||||
description: 'Test',
|
||||
};
|
||||
|
||||
expect(options.image?.url).toBeUndefined();
|
||||
expect(options.image?.alt).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user