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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user