import { describe, it, expect, } from 'vitest'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue'; import type { IconName } from '@/presentation/components/Shared/Icon/IconName'; import { UseSvgLoaderStub } from '@tests/unit/shared/Stubs/UseSvgLoaderStub'; import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage'; describe('AppIcon.vue', () => { it('renders the correct SVG content based on the icon prop', async () => { // arrange const expectedIconName: IconName = 'magnifying-glass'; const expectedIconContent = ''; const svgLoaderStub = new UseSvgLoaderStub(); svgLoaderStub.withSvgIcon(expectedIconName, expectedIconContent); // act const wrapper = mountComponent({ iconPropValue: expectedIconName, loader: svgLoaderStub, }); await nextTick(); // assert const actualSvg = wrapper.html(); expectSvg(actualSvg, expectedIconContent); }); it('updates the SVG content when the icon prop changes', async () => { // arrange const initialIconName: IconName = 'magnifying-glass'; const updatedIconName: IconName = 'copy'; const updatedIconContent = ''; const svgLoaderStub = new UseSvgLoaderStub(); svgLoaderStub.withSvgIcon(initialIconName, ''); svgLoaderStub.withSvgIcon(updatedIconName, updatedIconContent); // act const wrapper = mountComponent({ iconPropValue: initialIconName, loader: svgLoaderStub, }); await wrapper.setProps({ icon: updatedIconName }); await nextTick(); // assert const actualSvg = wrapper.html(); expectSvg(actualSvg, updatedIconContent); }); it('emits `click` event when clicked', async () => { // arrange const wrapper = mountComponent(); // act await wrapper.trigger('click'); await nextTick(); // assert expect(wrapper.emitted().click).to.have.lengthOf(1); }); }); function mountComponent(options?: { readonly iconPropValue?: IconName, readonly loader?: UseSvgLoaderStub, }) { const iconName = options?.iconPropValue ?? 'globe'; const loaderStub = options?.loader ?? new UseSvgLoaderStub().withSvgIcon(iconName, ''); return shallowMount(AppIcon, { props: { icon: iconName, }, global: { provide: { useSvgLoaderHook: loaderStub.get(), }, }, }); } function expectSvg(actualSvg: string, expectedSvg: string): ReturnType { const normalizedExpectedSvg = extractAndNormalizeSvg(expectedSvg); const normalizedActualSvg = extractAndNormalizeSvg(actualSvg); return expect(normalizedActualSvg).to.equal(normalizedExpectedSvg, formatAssertionMessage([ 'Expected:\n', normalizedExpectedSvg, 'Actual:\n', normalizedActualSvg, ])); } function extractAndNormalizeSvg(svgString: string): string { const svg = extractSvg(svgString); return normalizeSvg(svg); } function extractSvg(svgString: string): string { const svgMatches = svgString.match(/|\/>)/g); if (!svgMatches || svgMatches.length === 0) { throw new Error(`No SVG found in: ${svgString}`); } if (svgMatches.length > 1) { throw new Error(`Multiple SVGs found in: ${svgString}`); } const svgContent = svgMatches[0]; return svgContent; } function normalizeSvg(svgString: string): string { return svgString .replace(/\n/g, '') // Remove newlines .replace(/\s+/g, ' ') // Replace all whitespace sequences with a single space .replace(/> <') // Remove spaces between tags .replace(/ <\//g, '/g, '/>') // Remove spaces before self-closing tag end .replace(/<(\w+)([^>]*)><\/\1>/g, '<$1$2/>') // Convert to self-closing SVG tags .trim(); // Remove leading and trailing spaces }