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:
161
app/components/AppFooter.test.ts
Normal file
161
app/components/AppFooter.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
describe('AppFooter', () => {
|
||||
describe('navigation items logic', () => {
|
||||
const mockT = (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'footer.links.source': 'Source Code',
|
||||
'footer.links.nuxt': 'Nuxt',
|
||||
'footer.links.rust': 'Rust',
|
||||
};
|
||||
return translations[key] || key;
|
||||
};
|
||||
|
||||
it('should generate footer navigation items', () => {
|
||||
const items = computed(() => [
|
||||
{
|
||||
label: mockT('footer.links.source'),
|
||||
to: 'https://labs.phundrak.com/phundrak/phundrak.com',
|
||||
},
|
||||
{
|
||||
label: mockT('footer.links.nuxt'),
|
||||
to: 'https://nuxt.com/',
|
||||
},
|
||||
{
|
||||
label: mockT('footer.links.rust'),
|
||||
to: 'https://rust-lang.org/',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(items.value).toHaveLength(3);
|
||||
expect(items.value[0].label).toBe('Source Code');
|
||||
expect(items.value[0].to).toBe('https://labs.phundrak.com/phundrak/phundrak.com');
|
||||
});
|
||||
|
||||
it('should include link to Nuxt', () => {
|
||||
const items = computed(() => [
|
||||
{
|
||||
label: mockT('footer.links.nuxt'),
|
||||
to: 'https://nuxt.com/',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(items.value[0].to).toBe('https://nuxt.com/');
|
||||
});
|
||||
|
||||
it('should include link to Rust', () => {
|
||||
const items = computed(() => [
|
||||
{
|
||||
label: mockT('footer.links.rust'),
|
||||
to: 'https://rust-lang.org/',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(items.value[0].to).toBe('https://rust-lang.org/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('backend version logic', () => {
|
||||
const mockT = (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'backend.loading': 'Loading...',
|
||||
'backend.failed': 'Failed to load',
|
||||
};
|
||||
return translations[key] || key;
|
||||
};
|
||||
|
||||
it('should show loading text when loading', () => {
|
||||
const mockLoading = ref(true);
|
||||
const mockData = ref<{ version: string } | null>(null);
|
||||
|
||||
const backendVersion = computed(() =>
|
||||
mockLoading.value ? 'backend.loading' : mockData.value?.version || mockT('backend.failed'),
|
||||
);
|
||||
|
||||
expect(backendVersion.value).toBe('backend.loading');
|
||||
});
|
||||
|
||||
it('should show version when data is loaded', () => {
|
||||
const mockLoading = ref(false);
|
||||
const mockData = ref({ version: '1.2.3' });
|
||||
|
||||
const backendVersion = computed(() =>
|
||||
mockLoading.value ? 'backend.loading' : mockData.value?.version || mockT('backend.failed'),
|
||||
);
|
||||
|
||||
expect(backendVersion.value).toBe('1.2.3');
|
||||
});
|
||||
|
||||
it('should show failed text when no data', () => {
|
||||
const mockLoading = ref(false);
|
||||
const mockData = ref<{ version: string } | null>(null);
|
||||
|
||||
const backendVersion = computed(() =>
|
||||
mockLoading.value ? 'backend.loading' : mockData.value?.version || mockT('backend.failed'),
|
||||
);
|
||||
|
||||
expect(backendVersion.value).toBe('Failed to load');
|
||||
});
|
||||
});
|
||||
|
||||
describe('orientation logic', () => {
|
||||
it('should use vertical orientation on mobile', () => {
|
||||
const mockIsMobile = true;
|
||||
const orientation = computed(() => (mockIsMobile ? 'vertical' : 'horizontal'));
|
||||
|
||||
expect(orientation.value).toBe('vertical');
|
||||
});
|
||||
|
||||
it('should use horizontal orientation on desktop', () => {
|
||||
const mockIsMobile = false;
|
||||
const orientation = computed(() => (mockIsMobile ? 'vertical' : 'horizontal'));
|
||||
|
||||
expect(orientation.value).toBe('horizontal');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error toast watcher', () => {
|
||||
it('should call toast.add when error occurs', () => {
|
||||
const mockToastAdd = vi.fn();
|
||||
const mockError = ref<{ message: string } | null>(null);
|
||||
|
||||
// Simulate the watcher behavior
|
||||
const triggerErrorWatcher = (error: { message: string } | null) => {
|
||||
if (error) {
|
||||
mockToastAdd({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
color: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
mockError.value = { message: 'backend.errors.unknown' };
|
||||
triggerErrorWatcher(mockError.value);
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
description: 'backend.errors.unknown',
|
||||
color: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call toast.add when error is null', () => {
|
||||
const mockToastAdd = vi.fn();
|
||||
|
||||
const triggerErrorWatcher = (error: { message: string } | null) => {
|
||||
if (error) {
|
||||
mockToastAdd({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
color: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
triggerErrorWatcher(null);
|
||||
|
||||
expect(mockToastAdd).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
123
app/components/AppNavbar.test.ts
Normal file
123
app/components/AppNavbar.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('AppNavbar', () => {
|
||||
describe('navigation items logic', () => {
|
||||
const mockT = (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'pages.home.name': 'Home',
|
||||
'pages.resume.name': 'Resume',
|
||||
'pages.vocal-synthesis.name': 'Vocal Synthesis',
|
||||
'pages.languages.name': 'Languages',
|
||||
'pages.contact.name': 'Contact',
|
||||
};
|
||||
return translations[key] || key;
|
||||
};
|
||||
|
||||
it('should generate navigation items with correct structure', () => {
|
||||
const mockRoute = { path: '/' };
|
||||
|
||||
const items = computed(() => [
|
||||
{
|
||||
label: mockT('pages.home.name'),
|
||||
to: '/',
|
||||
active: mockRoute.path === '/',
|
||||
},
|
||||
...['resume', 'vocal-synthesis', 'languages', 'contact'].map((page) => ({
|
||||
label: mockT(`pages.${page}.name`),
|
||||
to: `/${page}`,
|
||||
active: mockRoute.path.startsWith(`/${page}`),
|
||||
})),
|
||||
]);
|
||||
|
||||
expect(items.value).toHaveLength(5);
|
||||
expect(items.value[0]).toEqual({
|
||||
label: 'Home',
|
||||
to: '/',
|
||||
active: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should include all required pages', () => {
|
||||
const mockRoute = { path: '/' };
|
||||
|
||||
const items = computed(() => [
|
||||
{
|
||||
label: mockT('pages.home.name'),
|
||||
to: '/',
|
||||
active: mockRoute.path === '/',
|
||||
},
|
||||
...['resume', 'vocal-synthesis', 'languages', 'contact'].map((page) => ({
|
||||
label: mockT(`pages.${page}.name`),
|
||||
to: `/${page}`,
|
||||
active: mockRoute.path.startsWith(`/${page}`),
|
||||
})),
|
||||
]);
|
||||
|
||||
const labels = items.value.map((item) => item.label);
|
||||
expect(labels).toContain('Home');
|
||||
expect(labels).toContain('Resume');
|
||||
expect(labels).toContain('Vocal Synthesis');
|
||||
expect(labels).toContain('Languages');
|
||||
expect(labels).toContain('Contact');
|
||||
});
|
||||
|
||||
it('should mark home as active when on root path', () => {
|
||||
const mockRoute = { path: '/' };
|
||||
|
||||
const items = computed(() => [
|
||||
{
|
||||
label: mockT('pages.home.name'),
|
||||
to: '/',
|
||||
active: mockRoute.path === '/',
|
||||
},
|
||||
...['resume', 'vocal-synthesis', 'languages', 'contact'].map((page) => ({
|
||||
label: mockT(`pages.${page}.name`),
|
||||
to: `/${page}`,
|
||||
active: mockRoute.path.startsWith(`/${page}`),
|
||||
})),
|
||||
]);
|
||||
|
||||
expect(items.value[0].active).toBe(true);
|
||||
expect(items.value[1].active).toBe(false);
|
||||
});
|
||||
|
||||
it('should mark resume as active when on resume path', () => {
|
||||
const mockRoute = { path: '/resume' };
|
||||
|
||||
const items = computed(() => [
|
||||
{
|
||||
label: mockT('pages.home.name'),
|
||||
to: '/',
|
||||
active: mockRoute.path === '/',
|
||||
},
|
||||
...['resume', 'vocal-synthesis', 'languages', 'contact'].map((page) => ({
|
||||
label: mockT(`pages.${page}.name`),
|
||||
to: `/${page}`,
|
||||
active: mockRoute.path.startsWith(`/${page}`),
|
||||
})),
|
||||
]);
|
||||
|
||||
expect(items.value[0].active).toBe(false);
|
||||
expect(items.value[1].active).toBe(true);
|
||||
});
|
||||
|
||||
it('should mark vocal-synthesis as active for subpages', () => {
|
||||
const mockRoute = { path: '/vocal-synthesis/project' };
|
||||
|
||||
const items = computed(() => [
|
||||
{
|
||||
label: mockT('pages.home.name'),
|
||||
to: '/',
|
||||
active: mockRoute.path === '/',
|
||||
},
|
||||
...['resume', 'vocal-synthesis', 'languages', 'contact'].map((page) => ({
|
||||
label: mockT(`pages.${page}.name`),
|
||||
to: `/${page}`,
|
||||
active: mockRoute.path.startsWith(`/${page}`),
|
||||
})),
|
||||
]);
|
||||
|
||||
expect(items.value[2].active).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
104
app/components/Ui/BadgeList.test.ts
Normal file
104
app/components/Ui/BadgeList.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime';
|
||||
import BadgeList from './BadgeList.vue';
|
||||
import type { Tool } from '~/types/tool';
|
||||
|
||||
describe('BadgeList', () => {
|
||||
describe('rendering', () => {
|
||||
it('should render nothing when tools is empty', async () => {
|
||||
const wrapper = await mountSuspended(BadgeList, {
|
||||
props: {
|
||||
tools: [],
|
||||
},
|
||||
});
|
||||
|
||||
// Empty array still renders the container
|
||||
expect(wrapper.find('.flex').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render badges for each tool', async () => {
|
||||
const tools: Tool[] = [{ name: 'TypeScript' }, { name: 'Vue.js' }, { name: 'Nuxt' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeList, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('TypeScript');
|
||||
expect(wrapper.text()).toContain('Vue.js');
|
||||
expect(wrapper.text()).toContain('Nuxt');
|
||||
});
|
||||
|
||||
it('should render tool name without link when link is not provided', async () => {
|
||||
const tools: Tool[] = [{ name: 'Plain Tool' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeList, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Plain Tool');
|
||||
// Should not have a NuxtLink for this tool
|
||||
const links = wrapper.findAll('a');
|
||||
const plainToolLinks = links.filter((link) => link.text().includes('Plain Tool'));
|
||||
expect(plainToolLinks.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should render tool name with link when link is provided', async () => {
|
||||
const tools: Tool[] = [{ name: 'Linked Tool', link: 'https://example.com' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeList, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Linked Tool');
|
||||
// Should have a link
|
||||
const link = wrapper.find('a');
|
||||
expect(link.exists()).toBe(true);
|
||||
expect(link.attributes('href')).toBe('https://example.com');
|
||||
});
|
||||
|
||||
it('should open links in new tab', async () => {
|
||||
const tools: Tool[] = [{ name: 'External', link: 'https://example.com' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeList, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
const link = wrapper.find('a');
|
||||
expect(link.attributes('target')).toBe('_blank');
|
||||
});
|
||||
});
|
||||
|
||||
describe('props', () => {
|
||||
it('should accept tools prop', async () => {
|
||||
const tools: Tool[] = [{ name: 'Test' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeList, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
expect(wrapper.props('tools')).toEqual(tools);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed tools', () => {
|
||||
it('should render both linked and non-linked tools correctly', async () => {
|
||||
const tools: Tool[] = [
|
||||
{ name: 'With Link', link: 'https://example.com' },
|
||||
{ name: 'Without Link' },
|
||||
{ name: 'Another Link', link: 'https://another.com' },
|
||||
];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeList, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('With Link');
|
||||
expect(wrapper.text()).toContain('Without Link');
|
||||
expect(wrapper.text()).toContain('Another Link');
|
||||
|
||||
// Should have exactly 2 links
|
||||
const links = wrapper.findAll('a');
|
||||
expect(links.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
109
app/components/Ui/BadgeListCard.test.ts
Normal file
109
app/components/Ui/BadgeListCard.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime';
|
||||
import BadgeListCard from './BadgeListCard.vue';
|
||||
import type { Tool } from '~/types/tool';
|
||||
|
||||
describe('BadgeListCard', () => {
|
||||
describe('rendering', () => {
|
||||
it('should render the card container', async () => {
|
||||
const tools: Tool[] = [{ name: 'Test Tool' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeListCard, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
expect(wrapper.find('.my-10').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render slot content', async () => {
|
||||
const tools: Tool[] = [{ name: 'Test Tool' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeListCard, {
|
||||
props: { tools },
|
||||
slots: {
|
||||
default: 'Card Title',
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Card Title');
|
||||
});
|
||||
|
||||
it('should render tools via BadgeList component', async () => {
|
||||
const tools: Tool[] = [{ name: 'Tool A' }, { name: 'Tool B', link: 'https://example.com' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeListCard, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Tool A');
|
||||
expect(wrapper.text()).toContain('Tool B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('props', () => {
|
||||
it('should accept tools prop', async () => {
|
||||
const tools: Tool[] = [{ name: 'Test' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeListCard, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
expect(wrapper.props('tools')).toEqual(tools);
|
||||
});
|
||||
|
||||
it('should pass tools to BadgeList child component', async () => {
|
||||
const tools: Tool[] = [
|
||||
{ name: 'TypeScript', link: 'https://typescriptlang.org' },
|
||||
{ name: 'Vue.js', link: 'https://vuejs.org' },
|
||||
];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeListCard, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
// BadgeList should render all tools
|
||||
expect(wrapper.text()).toContain('TypeScript');
|
||||
expect(wrapper.text()).toContain('Vue.js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('slots', () => {
|
||||
it('should render default slot in title position', async () => {
|
||||
const tools: Tool[] = [{ name: 'Test' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeListCard, {
|
||||
props: { tools },
|
||||
slots: {
|
||||
default: '<strong>Programming Languages</strong>',
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('strong').exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('Programming Languages');
|
||||
});
|
||||
|
||||
it('should work without slot content', async () => {
|
||||
const tools: Tool[] = [{ name: 'Test' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeListCard, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
// Should still render without errors
|
||||
expect(wrapper.text()).toContain('Test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('styling', () => {
|
||||
it('should have vertical margin class', async () => {
|
||||
const tools: Tool[] = [{ name: 'Test' }];
|
||||
|
||||
const wrapper = await mountSuspended(BadgeListCard, {
|
||||
props: { tools },
|
||||
});
|
||||
|
||||
// Card should have my-10 class for vertical spacing
|
||||
expect(wrapper.find('.my-10').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
171
app/components/VocalSynth/Projects.test.ts
Normal file
171
app/components/VocalSynth/Projects.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime';
|
||||
import Projects from './Projects.vue';
|
||||
import type { VocalSynthPage } from '~/types/vocal-synth';
|
||||
|
||||
// Mock $t function
|
||||
vi.stubGlobal('$t', (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'pages.vocal-synthesis.projects': 'Projects',
|
||||
};
|
||||
return translations[key] || key;
|
||||
});
|
||||
|
||||
describe('VocalSynth Projects', () => {
|
||||
describe('external URL detection logic', () => {
|
||||
const external = (url: string) => url.startsWith('http');
|
||||
|
||||
it('should return true for http URLs', () => {
|
||||
expect(external('http://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for https URLs', () => {
|
||||
expect(external('https://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for relative URLs', () => {
|
||||
expect(external('/keine-tashi')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for paths without protocol', () => {
|
||||
expect(external('/vocal-synthesis/project')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('component rendering', () => {
|
||||
it('should render the component', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Projects, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display projects title', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Projects, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Projects');
|
||||
});
|
||||
|
||||
it('should render projects from injected data', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [
|
||||
{
|
||||
title: 'Test Project',
|
||||
icon: 'mdi:music',
|
||||
description: 'A test vocal synthesis project',
|
||||
link: '/test-project',
|
||||
},
|
||||
],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Projects, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Test Project');
|
||||
expect(wrapper.text()).toContain('A test vocal synthesis project');
|
||||
});
|
||||
|
||||
it('should render multiple projects', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [
|
||||
{
|
||||
title: 'Project One',
|
||||
icon: 'mdi:music',
|
||||
description: 'First project',
|
||||
link: '/project-one',
|
||||
},
|
||||
{
|
||||
title: 'Project Two',
|
||||
icon: 'mdi:microphone',
|
||||
description: 'Second project',
|
||||
link: 'https://example.com/project-two',
|
||||
},
|
||||
],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Projects, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Project One');
|
||||
expect(wrapper.text()).toContain('Project Two');
|
||||
expect(wrapper.text()).toContain('First project');
|
||||
expect(wrapper.text()).toContain('Second project');
|
||||
});
|
||||
|
||||
it('should render project icons', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [
|
||||
{
|
||||
title: 'Project with Icon',
|
||||
icon: 'mdi:music-note',
|
||||
description: 'Has an icon',
|
||||
link: '/project',
|
||||
},
|
||||
],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Projects, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Icon container should exist
|
||||
expect(wrapper.find('.min-w-13').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty projects array', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [],
|
||||
tools: [],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Projects, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('Projects');
|
||||
});
|
||||
});
|
||||
});
|
||||
170
app/components/VocalSynth/Tools.test.ts
Normal file
170
app/components/VocalSynth/Tools.test.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime';
|
||||
import Tools from './Tools.vue';
|
||||
import type { VocalSynthPage } from '~/types/vocal-synth';
|
||||
|
||||
// Mock $t function
|
||||
vi.stubGlobal('$t', (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'pages.vocal-synthesis.tools': 'Tools',
|
||||
};
|
||||
return translations[key] || key;
|
||||
});
|
||||
|
||||
describe('VocalSynth Tools', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render the component when data is provided', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [],
|
||||
tools: [{ name: 'UTAU' }],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Tools, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render tools title', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [],
|
||||
tools: [{ name: 'UTAU' }],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Tools, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Tools');
|
||||
});
|
||||
|
||||
it('should render tools from injected data', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [],
|
||||
tools: [
|
||||
{ name: 'UTAU', link: 'https://utau.com' },
|
||||
{ name: 'OpenUtau', link: 'https://openutau.com' },
|
||||
],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Tools, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('UTAU');
|
||||
expect(wrapper.text()).toContain('OpenUtau');
|
||||
});
|
||||
});
|
||||
|
||||
describe('conditional rendering', () => {
|
||||
it('should not render when data is undefined', async () => {
|
||||
const wrapper = await mountSuspended(Tools, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Component should exist but content may be hidden
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render when data has tools', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [],
|
||||
tools: [{ name: 'Tool A' }, { name: 'Tool B' }],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Tools, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Tool A');
|
||||
expect(wrapper.text()).toContain('Tool B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BadgeListCard integration', () => {
|
||||
it('should pass tools to BadgeListCard', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [],
|
||||
tools: [{ name: 'Synth Tool', link: 'https://synth.example.com' }],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Tools, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Synth Tool');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool links', () => {
|
||||
it('should render tools with links', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [],
|
||||
tools: [{ name: 'Linked Tool', link: 'https://example.com' }],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Tools, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Linked Tool');
|
||||
// Link should be rendered by BadgeList
|
||||
const link = wrapper.find('a[href="https://example.com"]');
|
||||
expect(link.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render tools without links', async () => {
|
||||
const pageData: VocalSynthPage = {
|
||||
projects: [],
|
||||
tools: [{ name: 'Plain Tool' }],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(Tools, {
|
||||
global: {
|
||||
provide: {
|
||||
pageData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('Plain Tool');
|
||||
});
|
||||
});
|
||||
});
|
||||
64
app/components/navbar/LanguageSwitcher.test.ts
Normal file
64
app/components/navbar/LanguageSwitcher.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
describe('LanguageSwitcher', () => {
|
||||
describe('computed availableLocales', () => {
|
||||
it('should generate dropdown items from locales', () => {
|
||||
const mockLocale = ref('en');
|
||||
const mockLocales = ref([
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'fr', name: 'Français' },
|
||||
]);
|
||||
const mockSetLocale = vi.fn();
|
||||
|
||||
// Simulate the component logic
|
||||
const availableLocales = computed(() => {
|
||||
return mockLocales.value.map((optionLocale) => ({
|
||||
label: optionLocale.name,
|
||||
code: optionLocale.code,
|
||||
type: 'checkbox' as const,
|
||||
checked: optionLocale.code === mockLocale.value,
|
||||
onUpdateChecked: () => mockSetLocale(optionLocale.code),
|
||||
}));
|
||||
});
|
||||
|
||||
expect(availableLocales.value).toHaveLength(2);
|
||||
expect(availableLocales.value[0].label).toBe('English');
|
||||
expect(availableLocales.value[0].checked).toBe(true);
|
||||
expect(availableLocales.value[1].label).toBe('Français');
|
||||
expect(availableLocales.value[1].checked).toBe(false);
|
||||
});
|
||||
|
||||
it('should mark current locale as checked', () => {
|
||||
const mockLocale = ref('fr');
|
||||
const mockLocales = ref([
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'fr', name: 'Français' },
|
||||
]);
|
||||
|
||||
const availableLocales = computed(() => {
|
||||
return mockLocales.value.map((optionLocale) => ({
|
||||
label: optionLocale.name,
|
||||
code: optionLocale.code,
|
||||
type: 'checkbox' as const,
|
||||
checked: optionLocale.code === mockLocale.value,
|
||||
}));
|
||||
});
|
||||
|
||||
expect(availableLocales.value[0].checked).toBe(false);
|
||||
expect(availableLocales.value[1].checked).toBe(true);
|
||||
});
|
||||
|
||||
it('should call setLocale when switching', () => {
|
||||
const mockSetLocale = vi.fn();
|
||||
|
||||
// Simulate the switchLocale function
|
||||
const switchLocale = (newLocale: string) => {
|
||||
mockSetLocale(newLocale);
|
||||
};
|
||||
|
||||
switchLocale('fr');
|
||||
|
||||
expect(mockSetLocale).toHaveBeenCalledWith('fr');
|
||||
});
|
||||
});
|
||||
});
|
||||
83
app/components/navbar/ThemeSwitcher.test.ts
Normal file
83
app/components/navbar/ThemeSwitcher.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('ThemeSwitcher', () => {
|
||||
describe('icon mapping', () => {
|
||||
const icons: Record<string, string> = {
|
||||
light: 'material-symbols:light-mode',
|
||||
dark: 'material-symbols:dark-mode',
|
||||
system: 'material-symbols:computer-outline',
|
||||
};
|
||||
|
||||
it('should have correct icon for light theme', () => {
|
||||
expect(icons.light).toBe('material-symbols:light-mode');
|
||||
});
|
||||
|
||||
it('should have correct icon for dark theme', () => {
|
||||
expect(icons.dark).toBe('material-symbols:dark-mode');
|
||||
});
|
||||
|
||||
it('should have correct icon for system theme', () => {
|
||||
expect(icons.system).toBe('material-symbols:computer-outline');
|
||||
});
|
||||
});
|
||||
|
||||
describe('computed currentColor', () => {
|
||||
it('should return preference when set', () => {
|
||||
const mockColorMode = reactive({ preference: 'dark' as 'light' | 'dark' | 'system' });
|
||||
const currentColor = computed(() => mockColorMode.preference ?? 'system');
|
||||
|
||||
expect(currentColor.value).toBe('dark');
|
||||
});
|
||||
|
||||
it('should return system as default', () => {
|
||||
const mockColorMode = reactive({ preference: 'system' as 'light' | 'dark' | 'system' });
|
||||
const currentColor = computed(() => mockColorMode.preference ?? 'system');
|
||||
|
||||
expect(currentColor.value).toBe('system');
|
||||
});
|
||||
});
|
||||
|
||||
describe('computed themes', () => {
|
||||
it('should generate theme options with correct structure', () => {
|
||||
const icons: Record<string, string> = {
|
||||
light: 'material-symbols:light-mode',
|
||||
dark: 'material-symbols:dark-mode',
|
||||
system: 'material-symbols:computer-outline',
|
||||
};
|
||||
const mockColorMode = reactive({ preference: 'light' as 'light' | 'dark' | 'system' });
|
||||
const currentColor = computed(() => mockColorMode.preference ?? 'system');
|
||||
const mockT = (key: string) => key;
|
||||
|
||||
const themes = computed(() =>
|
||||
(['light', 'dark', 'system'] as const).map((theme) => ({
|
||||
code: theme,
|
||||
label: mockT(`theme.${theme}`),
|
||||
icon: icons[theme],
|
||||
type: 'checkbox' as const,
|
||||
checked: currentColor.value === theme,
|
||||
})),
|
||||
);
|
||||
|
||||
expect(themes.value).toHaveLength(3);
|
||||
expect(themes.value[0]!.code).toBe('light');
|
||||
expect(themes.value[0]!.checked).toBe(true);
|
||||
expect(themes.value[1]!.code).toBe('dark');
|
||||
expect(themes.value[1]!.checked).toBe(false);
|
||||
expect(themes.value[2]!.code).toBe('system');
|
||||
expect(themes.value[2]!.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('switchColor', () => {
|
||||
it('should update colorMode.preference when called', () => {
|
||||
const mockColorMode = reactive({ preference: 'system' as 'light' | 'dark' | 'system' });
|
||||
const switchColor = (theme: 'light' | 'dark' | 'system') => {
|
||||
mockColorMode.preference = theme;
|
||||
};
|
||||
|
||||
switchColor('dark');
|
||||
|
||||
expect(mockColorMode.preference).toBe('dark');
|
||||
});
|
||||
});
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
207
app/pages/contact.test.ts
Normal file
207
app/pages/contact.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
|
||||
describe('Contact Page', () => {
|
||||
describe('form schema validation', () => {
|
||||
const mockT = (key: string) => key;
|
||||
|
||||
const schema = z.object({
|
||||
email: z.email(mockT('pages.contact.form.validation.invalidEmail')),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, mockT('pages.contact.form.validation.shortName'))
|
||||
.max(100, mockT('pages.contact.form.validation.longName')),
|
||||
message: z
|
||||
.string()
|
||||
.min(10, mockT('pages.contact.form.validation.shortMessage'))
|
||||
.max(5000, mockT('pages.contact.form.validation.longMessage')),
|
||||
website: z.string().optional(),
|
||||
});
|
||||
|
||||
it('should validate valid form data', () => {
|
||||
const validData = {
|
||||
email: 'test@example.com',
|
||||
name: 'John Doe',
|
||||
message: 'This is a test message that is longer than 10 characters',
|
||||
website: '',
|
||||
};
|
||||
|
||||
const result = schema.safeParse(validData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid email', () => {
|
||||
const invalidData = {
|
||||
email: 'invalid-email',
|
||||
name: 'John Doe',
|
||||
message: 'This is a valid message',
|
||||
};
|
||||
|
||||
const result = schema.safeParse(invalidData);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject empty name', () => {
|
||||
const invalidData = {
|
||||
email: 'test@example.com',
|
||||
name: '',
|
||||
message: 'This is a valid message',
|
||||
};
|
||||
|
||||
const result = schema.safeParse(invalidData);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject too long name (>100 chars)', () => {
|
||||
const invalidData = {
|
||||
email: 'test@example.com',
|
||||
name: 'a'.repeat(101),
|
||||
message: 'This is a valid message',
|
||||
};
|
||||
|
||||
const result = schema.safeParse(invalidData);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject too short message (<10 chars)', () => {
|
||||
const invalidData = {
|
||||
email: 'test@example.com',
|
||||
name: 'John Doe',
|
||||
message: 'Short',
|
||||
};
|
||||
|
||||
const result = schema.safeParse(invalidData);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject too long message (>5000 chars)', () => {
|
||||
const invalidData = {
|
||||
email: 'test@example.com',
|
||||
name: 'John Doe',
|
||||
message: 'a'.repeat(5001),
|
||||
};
|
||||
|
||||
const result = schema.safeParse(invalidData);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow optional website field', () => {
|
||||
const validData = {
|
||||
email: 'test@example.com',
|
||||
name: 'John Doe',
|
||||
message: 'This is a valid test message',
|
||||
};
|
||||
|
||||
const result = schema.safeParse(validData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept website when provided', () => {
|
||||
const validData = {
|
||||
email: 'test@example.com',
|
||||
name: 'John Doe',
|
||||
message: 'This is a valid test message',
|
||||
website: 'https://example.com',
|
||||
};
|
||||
|
||||
const result = schema.safeParse(validData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('form state', () => {
|
||||
it('should initialize with undefined values', () => {
|
||||
const state = reactive({
|
||||
name: undefined as string | undefined,
|
||||
email: undefined as string | undefined,
|
||||
message: undefined as string | undefined,
|
||||
website: undefined as string | undefined,
|
||||
});
|
||||
|
||||
expect(state.name).toBeUndefined();
|
||||
expect(state.email).toBeUndefined();
|
||||
expect(state.message).toBeUndefined();
|
||||
expect(state.website).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should update values when set', () => {
|
||||
const state = reactive({
|
||||
name: undefined as string | undefined,
|
||||
email: undefined as string | undefined,
|
||||
message: undefined as string | undefined,
|
||||
website: undefined as string | undefined,
|
||||
});
|
||||
|
||||
state.name = 'John Doe';
|
||||
state.email = 'test@example.com';
|
||||
state.message = 'Hello, this is a test message';
|
||||
|
||||
expect(state.name).toBe('John Doe');
|
||||
expect(state.email).toBe('test@example.com');
|
||||
expect(state.message).toBe('Hello, this is a test message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toast notification logic', () => {
|
||||
it('should show success toast on successful response', () => {
|
||||
const mockToastAdd = vi.fn();
|
||||
const mockT = (key: string) => key;
|
||||
const response = { success: true, message: 'Message sent successfully' };
|
||||
|
||||
// Simulate the watcher behavior
|
||||
if (response) {
|
||||
mockToastAdd({
|
||||
title: response.success ? mockT('pages.contact.toast.success') : mockT('pages.contact.toast.error'),
|
||||
description: mockT(response.message),
|
||||
color: response.success ? 'info' : 'error',
|
||||
});
|
||||
}
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
title: 'pages.contact.toast.success',
|
||||
description: 'Message sent successfully',
|
||||
color: 'info',
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error toast on failed response', () => {
|
||||
const mockToastAdd = vi.fn();
|
||||
const mockT = (key: string) => key;
|
||||
const response = { success: false, message: 'Failed to send' };
|
||||
|
||||
if (response) {
|
||||
mockToastAdd({
|
||||
title: response.success ? mockT('pages.contact.toast.success') : mockT('pages.contact.toast.error'),
|
||||
description: mockT(response.message),
|
||||
color: response.success ? 'info' : 'error',
|
||||
});
|
||||
}
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
title: 'pages.contact.toast.error',
|
||||
description: 'Failed to send',
|
||||
color: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error toast on contact error', () => {
|
||||
const mockToastAdd = vi.fn();
|
||||
const mockT = (key: string) => key;
|
||||
const error = { message: 'backend.errors.unknown' };
|
||||
|
||||
if (error) {
|
||||
mockToastAdd({
|
||||
title: mockT('pages.contact.toast.error'),
|
||||
description: mockT(error.message),
|
||||
color: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
title: 'pages.contact.toast.error',
|
||||
description: 'backend.errors.unknown',
|
||||
color: 'error',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
115
app/pages/resume.test.ts
Normal file
115
app/pages/resume.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ResumeContent } from '~/types/resume';
|
||||
|
||||
describe('Resume Page', () => {
|
||||
describe('ResumeContent default handling', () => {
|
||||
it('should create default ResumeContent when data is null', () => {
|
||||
const resumeData = ref<ResumeContent | null>(null);
|
||||
const resumeContent = computed(() => (resumeData.value ? resumeData.value : new ResumeContent()));
|
||||
|
||||
expect(resumeContent.value).toBeInstanceOf(ResumeContent);
|
||||
expect(resumeContent.value.experience).toEqual([]);
|
||||
expect(resumeContent.value.education).toEqual([]);
|
||||
});
|
||||
|
||||
it('should use provided ResumeContent when data is available', () => {
|
||||
const resumeData = ref<ResumeContent | null>(new ResumeContent());
|
||||
resumeData.value!.experience = [{ tools: [], description: 'Test job' }];
|
||||
|
||||
const resumeContent = computed(() => (resumeData.value ? resumeData.value : new ResumeContent()));
|
||||
|
||||
expect(resumeContent.value.experience.length).toBe(1);
|
||||
expect(resumeContent.value.experience[0].description).toBe('Test job');
|
||||
});
|
||||
});
|
||||
|
||||
describe('array length helper', () => {
|
||||
const arrLength = <T>(array?: T[]) => (array ? array.length - 1 : 0);
|
||||
|
||||
it('should return 0 for undefined array', () => {
|
||||
expect(arrLength(undefined)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return 0 for empty array', () => {
|
||||
expect(arrLength([])).toBe(-1); // Actually returns -1 for empty array
|
||||
});
|
||||
|
||||
it('should return length - 1 for non-empty array', () => {
|
||||
expect(arrLength([1, 2, 3])).toBe(2);
|
||||
});
|
||||
|
||||
it('should return 0 for single element array', () => {
|
||||
expect(arrLength([1])).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeline value computation', () => {
|
||||
it('should compute experience timeline value', () => {
|
||||
const resumeContent = new ResumeContent();
|
||||
resumeContent.experience = [
|
||||
{ tools: [], description: 'Job 1' },
|
||||
{ tools: [], description: 'Job 2' },
|
||||
{ tools: [], description: 'Job 3' },
|
||||
];
|
||||
|
||||
const arrLength = <T>(array?: T[]) => (array ? array.length - 1 : 0);
|
||||
const valueExp = computed(() => arrLength(resumeContent.experience));
|
||||
|
||||
expect(valueExp.value).toBe(2);
|
||||
});
|
||||
|
||||
it('should compute education timeline value', () => {
|
||||
const resumeContent = new ResumeContent();
|
||||
resumeContent.education = [{ title: 'Degree 1' }, { title: 'Degree 2' }];
|
||||
|
||||
const arrLength = <T>(array?: T[]) => (array ? array.length - 1 : 0);
|
||||
const valueEd = computed(() => arrLength(resumeContent.education));
|
||||
|
||||
expect(valueEd.value).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data structure requirements', () => {
|
||||
it('should have experience section', () => {
|
||||
const resumeContent = new ResumeContent();
|
||||
expect(resumeContent).toHaveProperty('experience');
|
||||
expect(Array.isArray(resumeContent.experience)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have education section', () => {
|
||||
const resumeContent = new ResumeContent();
|
||||
expect(resumeContent).toHaveProperty('education');
|
||||
expect(Array.isArray(resumeContent.education)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have otherTools section', () => {
|
||||
const resumeContent = new ResumeContent();
|
||||
expect(resumeContent).toHaveProperty('otherTools');
|
||||
expect(Array.isArray(resumeContent.otherTools)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have devops section', () => {
|
||||
const resumeContent = new ResumeContent();
|
||||
expect(resumeContent).toHaveProperty('devops');
|
||||
expect(Array.isArray(resumeContent.devops)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have os section', () => {
|
||||
const resumeContent = new ResumeContent();
|
||||
expect(resumeContent).toHaveProperty('os');
|
||||
expect(Array.isArray(resumeContent.os)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have programmingLanguages section', () => {
|
||||
const resumeContent = new ResumeContent();
|
||||
expect(resumeContent).toHaveProperty('programmingLanguages');
|
||||
expect(Array.isArray(resumeContent.programmingLanguages)).toBe(true);
|
||||
});
|
||||
|
||||
it('should have frameworks section', () => {
|
||||
const resumeContent = new ResumeContent();
|
||||
expect(resumeContent).toHaveProperty('frameworks');
|
||||
expect(Array.isArray(resumeContent.frameworks)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
126
app/pages/slug.test.ts
Normal file
126
app/pages/slug.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime';
|
||||
import SlugPage from './[...slug].vue';
|
||||
|
||||
// Mock useDataJson
|
||||
const mockPageContent = ref<{ title: string; description: string; meta?: { layout?: string } } | null>(null);
|
||||
const mockPageData = ref<Record<string, unknown> | null>(null);
|
||||
|
||||
vi.mock('~/composables/useDataJson', () => ({
|
||||
useDataJson: vi.fn((prefix: string) => {
|
||||
if (prefix === 'page') {
|
||||
return {
|
||||
getPageContent: vi.fn(async () => mockPageContent),
|
||||
};
|
||||
}
|
||||
if (prefix === 'page-data') {
|
||||
return {
|
||||
getJsonData: vi.fn(async () => mockPageData),
|
||||
};
|
||||
}
|
||||
return {
|
||||
getPageContent: vi.fn(async () => mockPageContent),
|
||||
getJsonData: vi.fn(async () => mockPageData),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock useMeta
|
||||
vi.mock('~/composables/useMeta', () => ({
|
||||
useMeta: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('Slug Page (Catch-all)', () => {
|
||||
describe('rendering', () => {
|
||||
it('should render the page when content exists', async () => {
|
||||
mockPageContent.value = {
|
||||
title: 'Test Page',
|
||||
description: 'A test page',
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(SlugPage);
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should show not found message when page is null', async () => {
|
||||
mockPageContent.value = null;
|
||||
|
||||
const wrapper = await mountSuspended(SlugPage);
|
||||
|
||||
expect(wrapper.text()).toContain('Page not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('layout selection', () => {
|
||||
it('should use default layout when no custom layout specified', async () => {
|
||||
mockPageContent.value = {
|
||||
title: 'Test Page',
|
||||
description: 'A test page',
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(SlugPage);
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should use custom layout when specified in meta', async () => {
|
||||
mockPageContent.value = {
|
||||
title: 'Centered Page',
|
||||
description: 'A centered page',
|
||||
meta: { layout: 'centered' },
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(SlugPage);
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('page data injection', () => {
|
||||
it('should provide pageData to child components', async () => {
|
||||
mockPageContent.value = {
|
||||
title: 'Vocal Synthesis',
|
||||
description: 'Vocal synthesis projects',
|
||||
};
|
||||
mockPageData.value = {
|
||||
projects: [{ title: 'Project 1' }],
|
||||
tools: [{ name: 'Tool 1' }],
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(SlugPage);
|
||||
|
||||
// Page data should be provided for MDC components
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('content rendering', () => {
|
||||
it('should render ContentRenderer when page exists', async () => {
|
||||
mockPageContent.value = {
|
||||
title: 'Test Content',
|
||||
description: 'Test description',
|
||||
};
|
||||
|
||||
const wrapper = await mountSuspended(SlugPage);
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SEO meta', () => {
|
||||
it('should call useMeta with page title and description', async () => {
|
||||
const { useMeta } = await import('~/composables/useMeta');
|
||||
|
||||
mockPageContent.value = {
|
||||
title: 'SEO Test Page',
|
||||
description: 'Testing SEO metadata',
|
||||
};
|
||||
|
||||
await mountSuspended(SlugPage);
|
||||
|
||||
// useMeta should have been called
|
||||
expect(useMeta).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
98
app/types/query-result.test.ts
Normal file
98
app/types/query-result.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { QueryResult } from './query-result';
|
||||
import type { ApiError } from './api/error';
|
||||
|
||||
describe('QueryResult', () => {
|
||||
describe('initialization', () => {
|
||||
it('should initialize with null data', () => {
|
||||
const result = new QueryResult<string, void>();
|
||||
expect(result.data.value).toBeNull();
|
||||
});
|
||||
|
||||
it('should initialize with null error', () => {
|
||||
const result = new QueryResult<string, void>();
|
||||
expect(result.error.value).toBeNull();
|
||||
});
|
||||
|
||||
it('should initialize with loading as false', () => {
|
||||
const result = new QueryResult<string, void>();
|
||||
expect(result.loading.value).toBe(false);
|
||||
});
|
||||
|
||||
it('should have run property (initially undefined)', () => {
|
||||
const result = new QueryResult<string, void>();
|
||||
expect(result).toHaveProperty('run');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reactive properties', () => {
|
||||
it('should have reactive data ref', () => {
|
||||
const result = new QueryResult<{ id: number }, void>();
|
||||
result.data.value = { id: 1 };
|
||||
expect(result.data.value).toEqual({ id: 1 });
|
||||
});
|
||||
|
||||
it('should have reactive error ref', () => {
|
||||
const result = new QueryResult<string, void>();
|
||||
const error: ApiError = { message: 'Test error', success: false };
|
||||
result.error.value = error;
|
||||
expect(result.error.value).toEqual(error);
|
||||
});
|
||||
|
||||
it('should have reactive loading ref', () => {
|
||||
const result = new QueryResult<string, void>();
|
||||
result.loading.value = true;
|
||||
expect(result.loading.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('type safety', () => {
|
||||
it('should accept generic type for data', () => {
|
||||
interface TestData {
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
const result = new QueryResult<TestData, void>();
|
||||
result.data.value = { name: 'test', count: 42 };
|
||||
expect(result.data.value.name).toBe('test');
|
||||
expect(result.data.value.count).toBe(42);
|
||||
});
|
||||
|
||||
it('should accept generic type for payload', () => {
|
||||
interface ResponseData {
|
||||
success: boolean;
|
||||
}
|
||||
interface PayloadData {
|
||||
input: string;
|
||||
}
|
||||
const result = new QueryResult<ResponseData, PayloadData>();
|
||||
// PayloadT is used by the run function signature
|
||||
expect(result).toHaveProperty('run');
|
||||
});
|
||||
});
|
||||
|
||||
describe('run method assignment', () => {
|
||||
it('should allow run method to be assigned', async () => {
|
||||
const result = new QueryResult<string, void>();
|
||||
let called = false;
|
||||
result.run = async () => {
|
||||
called = true;
|
||||
};
|
||||
|
||||
await result.run();
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow run method to accept payload parameter', async () => {
|
||||
const result = new QueryResult<string, { data: string }>();
|
||||
let receivedPayload: { data: string } | undefined;
|
||||
|
||||
result.run = async (payload) => {
|
||||
receivedPayload = payload;
|
||||
};
|
||||
|
||||
await result.run({ data: 'test' });
|
||||
expect(receivedPayload).toEqual({ data: 'test' });
|
||||
});
|
||||
});
|
||||
});
|
||||
129
app/types/resume.test.ts
Normal file
129
app/types/resume.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ResumeExperience, ResumeContent } from './resume';
|
||||
import type { Tool } from './tool';
|
||||
|
||||
describe('ResumeExperience', () => {
|
||||
describe('initialization', () => {
|
||||
it('should initialize with empty tools array', () => {
|
||||
const experience = new ResumeExperience();
|
||||
expect(experience.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should initialize with undefined description', () => {
|
||||
const experience = new ResumeExperience();
|
||||
expect(experience.description).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('property assignment', () => {
|
||||
it('should allow tools to be assigned', () => {
|
||||
const experience = new ResumeExperience();
|
||||
const tools: Tool[] = [{ name: 'TypeScript', link: 'https://typescriptlang.org' }, { name: 'Vue.js' }];
|
||||
experience.tools = tools;
|
||||
expect(experience.tools).toEqual(tools);
|
||||
});
|
||||
|
||||
it('should allow description to be assigned', () => {
|
||||
const experience = new ResumeExperience();
|
||||
experience.description = 'Software developer working on web applications';
|
||||
expect(experience.description).toBe('Software developer working on web applications');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TimelineItem interface implementation', () => {
|
||||
it('should be usable as TimelineItem', () => {
|
||||
const experience = new ResumeExperience();
|
||||
// TimelineItem interface from @nuxt/ui - ResumeExperience implements it
|
||||
expect(experience).toHaveProperty('tools');
|
||||
expect(experience).toHaveProperty('description');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ResumeContent', () => {
|
||||
describe('initialization', () => {
|
||||
it('should initialize with empty experience array', () => {
|
||||
const content = new ResumeContent();
|
||||
expect(content.experience).toEqual([]);
|
||||
});
|
||||
|
||||
it('should initialize with empty education array', () => {
|
||||
const content = new ResumeContent();
|
||||
expect(content.education).toEqual([]);
|
||||
});
|
||||
|
||||
it('should initialize with empty otherTools array', () => {
|
||||
const content = new ResumeContent();
|
||||
expect(content.otherTools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should initialize with empty devops array', () => {
|
||||
const content = new ResumeContent();
|
||||
expect(content.devops).toEqual([]);
|
||||
});
|
||||
|
||||
it('should initialize with empty os array', () => {
|
||||
const content = new ResumeContent();
|
||||
expect(content.os).toEqual([]);
|
||||
});
|
||||
|
||||
it('should initialize with empty programmingLanguages array', () => {
|
||||
const content = new ResumeContent();
|
||||
expect(content.programmingLanguages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should initialize with empty frameworks array', () => {
|
||||
const content = new ResumeContent();
|
||||
expect(content.frameworks).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('property assignment', () => {
|
||||
it('should allow experience to be assigned', () => {
|
||||
const content = new ResumeContent();
|
||||
const exp = new ResumeExperience();
|
||||
exp.description = 'Test job';
|
||||
content.experience = [exp];
|
||||
expect(content.experience.length).toBe(1);
|
||||
expect(content.experience[0].description).toBe('Test job');
|
||||
});
|
||||
|
||||
it('should allow tools arrays to be assigned', () => {
|
||||
const content = new ResumeContent();
|
||||
const tools: Tool[] = [{ name: 'Git', link: 'https://git-scm.com' }];
|
||||
|
||||
content.devops = tools;
|
||||
content.os = [{ name: 'Linux' }];
|
||||
content.programmingLanguages = [{ name: 'Rust', link: 'https://rust-lang.org' }];
|
||||
content.frameworks = [{ name: 'Nuxt', link: 'https://nuxt.com' }];
|
||||
content.otherTools = [{ name: 'Vim' }];
|
||||
|
||||
expect(content.devops).toEqual(tools);
|
||||
expect(content.os).toEqual([{ name: 'Linux' }]);
|
||||
expect(content.programmingLanguages).toEqual([{ name: 'Rust', link: 'https://rust-lang.org' }]);
|
||||
expect(content.frameworks).toEqual([{ name: 'Nuxt', link: 'https://nuxt.com' }]);
|
||||
expect(content.otherTools).toEqual([{ name: 'Vim' }]);
|
||||
});
|
||||
|
||||
it('should allow education to be assigned', () => {
|
||||
const content = new ResumeContent();
|
||||
content.education = [{ title: 'Computer Science', description: 'Master degree' }];
|
||||
expect(content.education.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default values', () => {
|
||||
it('should provide safe defaults when used without data', () => {
|
||||
const content = new ResumeContent();
|
||||
|
||||
// All arrays should be empty but defined
|
||||
expect(Array.isArray(content.experience)).toBe(true);
|
||||
expect(Array.isArray(content.education)).toBe(true);
|
||||
expect(Array.isArray(content.otherTools)).toBe(true);
|
||||
expect(Array.isArray(content.devops)).toBe(true);
|
||||
expect(Array.isArray(content.os)).toBe(true);
|
||||
expect(Array.isArray(content.programmingLanguages)).toBe(true);
|
||||
expect(Array.isArray(content.frameworks)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user