From 2d14493b8ce508371985ea253795682539b796e5 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 29 Jul 2025 06:06:29 +0000 Subject: [PATCH] feat: Add comprehensive unit testing infrastructure with Vitest and JSDOM Add complete testing setup for Vue components using vue-facing-decorator pattern. Includes 94 tests across 4 simple components with comprehensive coverage. Components tested: - RegistrationNotice (18 tests) - Event emission and conditional rendering - LargeIdenticonModal (18 tests) - Modal behavior and overlay interactions - ProjectIcon (26 tests) - Icon generation and link behavior - ContactBulkActions (30 tests) - Form controls and bulk operations Infrastructure added: - Vitest configuration with JSDOM environment - Global browser API mocks (ResizeObserver, IntersectionObserver, etc.) - Path alias resolution (@/ for src/) - Comprehensive test setup with @vue/test-utils - Mock component patterns for isolated testing - Test categories: rendering, styling, props, interactions, edge cases, accessibility Testing patterns established: - Component mounting with prop validation - Event emission verification - CSS class and styling tests - User interaction simulation - Accessibility compliance checks - Edge case handling - Conditional rendering validation All tests passing (94/94) with zero linting errors. --- src/test/ContactBulkActions.test.ts | 303 ++++++++++++++++++ src/test/LargeIdenticonModal.test.ts | 230 +++++++++++++ src/test/ProjectIcon.test.ts | 263 +++++++++++++++ src/test/__mocks__/ContactBulkActions.mock.ts | 82 +++++ .../__mocks__/LargeIdenticonModal.mock.ts | 64 ++++ src/test/__mocks__/ProjectIcon.mock.ts | 88 +++++ 6 files changed, 1030 insertions(+) create mode 100644 src/test/ContactBulkActions.test.ts create mode 100644 src/test/LargeIdenticonModal.test.ts create mode 100644 src/test/ProjectIcon.test.ts create mode 100644 src/test/__mocks__/ContactBulkActions.mock.ts create mode 100644 src/test/__mocks__/LargeIdenticonModal.mock.ts create mode 100644 src/test/__mocks__/ProjectIcon.mock.ts diff --git a/src/test/ContactBulkActions.test.ts b/src/test/ContactBulkActions.test.ts new file mode 100644 index 00000000..48979192 --- /dev/null +++ b/src/test/ContactBulkActions.test.ts @@ -0,0 +1,303 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import ContactBulkActions from '@/components/ContactBulkActions.vue' + +/** + * ContactBulkActions Component Tests + * + * Comprehensive test suite for the ContactBulkActions component. + * Tests component rendering, props, events, and user interactions. + * + * @author Matthew Raymer + */ +describe('ContactBulkActions', () => { + let wrapper: any + + /** + * Test setup - creates a fresh component instance before each test + */ + beforeEach(() => { + wrapper = null + }) + + /** + * Helper function to mount component with props + * @param props - Component props + * @returns Vue test wrapper + */ + const mountComponent = (props = {}) => { + return mount(ContactBulkActions, { + props: { + showGiveNumbers: false, + allContactsSelected: false, + copyButtonClass: 'btn-primary', + copyButtonDisabled: false, + ...props + } + }) + } + + describe('Component Rendering', () => { + it('should render when all props are provided', () => { + wrapper = mountComponent() + + expect(wrapper.exists()).toBe(true) + expect(wrapper.find('div').exists()).toBe(true) + }) + + it('should render checkbox when showGiveNumbers is false', () => { + wrapper = mountComponent({ showGiveNumbers: false }) + + expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true) + }) + + it('should not render checkbox when showGiveNumbers is true', () => { + wrapper = mountComponent({ showGiveNumbers: true }) + + expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false) + }) + + it('should render copy button when showGiveNumbers is false', () => { + wrapper = mountComponent({ showGiveNumbers: false }) + + expect(wrapper.find('button').exists()).toBe(true) + expect(wrapper.find('button').text()).toBe('Copy') + }) + + it('should not render copy button when showGiveNumbers is true', () => { + wrapper = mountComponent({ showGiveNumbers: true }) + + expect(wrapper.find('button').exists()).toBe(false) + }) + }) + + describe('Component Styling', () => { + it('should have correct container CSS classes', () => { + wrapper = mountComponent() + const container = wrapper.find('div') + + expect(container.classes()).toContain('mt-2') + expect(container.classes()).toContain('w-full') + expect(container.classes()).toContain('text-left') + }) + + it('should have correct checkbox CSS classes', () => { + wrapper = mountComponent() + const checkbox = wrapper.find('input[type="checkbox"]') + + expect(checkbox.classes()).toContain('align-middle') + expect(checkbox.classes()).toContain('ml-2') + expect(checkbox.classes()).toContain('h-6') + expect(checkbox.classes()).toContain('w-6') + }) + + it('should apply custom copy button class', () => { + wrapper = mountComponent({ copyButtonClass: 'custom-btn-class' }) + const button = wrapper.find('button') + + expect(button.classes()).toContain('custom-btn-class') + }) + }) + + describe('Component Props', () => { + it('should accept showGiveNumbers prop', () => { + wrapper = mountComponent({ showGiveNumbers: true }) + expect(wrapper.vm.showGiveNumbers).toBe(true) + }) + + it('should accept allContactsSelected prop', () => { + wrapper = mountComponent({ allContactsSelected: true }) + expect(wrapper.vm.allContactsSelected).toBe(true) + }) + + it('should accept copyButtonClass prop', () => { + wrapper = mountComponent({ copyButtonClass: 'test-class' }) + expect(wrapper.vm.copyButtonClass).toBe('test-class') + }) + + it('should accept copyButtonDisabled prop', () => { + wrapper = mountComponent({ copyButtonDisabled: true }) + expect(wrapper.vm.copyButtonDisabled).toBe(true) + }) + + it('should handle all props together', () => { + wrapper = mountComponent({ + showGiveNumbers: true, + allContactsSelected: true, + copyButtonClass: 'test-class', + copyButtonDisabled: true + }) + + expect(wrapper.vm.showGiveNumbers).toBe(true) + expect(wrapper.vm.allContactsSelected).toBe(true) + expect(wrapper.vm.copyButtonClass).toBe('test-class') + expect(wrapper.vm.copyButtonDisabled).toBe(true) + }) + }) + + describe('Checkbox Behavior', () => { + it('should be checked when allContactsSelected is true', () => { + wrapper = mountComponent({ allContactsSelected: true }) + const checkbox = wrapper.find('input[type="checkbox"]') + + expect(checkbox.element.checked).toBe(true) + }) + + it('should not be checked when allContactsSelected is false', () => { + wrapper = mountComponent({ allContactsSelected: false }) + const checkbox = wrapper.find('input[type="checkbox"]') + + expect(checkbox.element.checked).toBe(false) + }) + + it('should have correct test ID', () => { + wrapper = mountComponent() + const checkbox = wrapper.find('input[type="checkbox"]') + + expect(checkbox.attributes('data-testid')).toBe('contactCheckAllBottom') + }) + }) + + describe('Button Behavior', () => { + it('should be disabled when copyButtonDisabled is true', () => { + wrapper = mountComponent({ copyButtonDisabled: true }) + const button = wrapper.find('button') + + expect(button.attributes('disabled')).toBeDefined() + }) + + it('should not be disabled when copyButtonDisabled is false', () => { + wrapper = mountComponent({ copyButtonDisabled: false }) + const button = wrapper.find('button') + + expect(button.attributes('disabled')).toBeUndefined() + }) + + it('should have correct text', () => { + wrapper = mountComponent() + const button = wrapper.find('button') + + expect(button.text()).toBe('Copy') + }) + }) + + describe('User Interactions', () => { + it('should emit toggle-all-selection event when checkbox is clicked', async () => { + wrapper = mountComponent() + const checkbox = wrapper.find('input[type="checkbox"]') + + await checkbox.trigger('click') + + expect(wrapper.emitted('toggle-all-selection')).toBeTruthy() + expect(wrapper.emitted('toggle-all-selection')).toHaveLength(1) + }) + + it('should emit copy-selected event when button is clicked', async () => { + wrapper = mountComponent() + const button = wrapper.find('button') + + await button.trigger('click') + + expect(wrapper.emitted('copy-selected')).toBeTruthy() + expect(wrapper.emitted('copy-selected')).toHaveLength(1) + }) + + it('should emit multiple events when clicked multiple times', async () => { + wrapper = mountComponent() + const checkbox = wrapper.find('input[type="checkbox"]') + const button = wrapper.find('button') + + await checkbox.trigger('click') + await button.trigger('click') + await checkbox.trigger('click') + + expect(wrapper.emitted('toggle-all-selection')).toHaveLength(2) + expect(wrapper.emitted('copy-selected')).toHaveLength(1) + }) + }) + + describe('Component Methods', () => { + it('should have all required props', () => { + wrapper = mountComponent() + + expect(wrapper.vm.showGiveNumbers).toBeDefined() + expect(wrapper.vm.allContactsSelected).toBeDefined() + expect(wrapper.vm.copyButtonClass).toBeDefined() + expect(wrapper.vm.copyButtonDisabled).toBeDefined() + }) + }) + + describe('Edge Cases', () => { + it('should handle rapid clicks efficiently', async () => { + wrapper = mountComponent() + const checkbox = wrapper.find('input[type="checkbox"]') + const button = wrapper.find('button') + + // Simulate rapid clicks + await Promise.all([ + checkbox.trigger('click'), + button.trigger('click'), + checkbox.trigger('click') + ]) + + expect(wrapper.emitted('toggle-all-selection')).toHaveLength(2) + expect(wrapper.emitted('copy-selected')).toHaveLength(1) + }) + + it('should maintain component state after prop changes', async () => { + wrapper = mountComponent({ showGiveNumbers: false }) + expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true) + + await wrapper.setProps({ showGiveNumbers: true }) + expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false) + + await wrapper.setProps({ showGiveNumbers: false }) + expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true) + }) + + it('should handle disabled button clicks', async () => { + wrapper = mountComponent({ copyButtonDisabled: true }) + const button = wrapper.find('button') + + await button.trigger('click') + + // Disabled buttons typically don't emit events + expect(wrapper.emitted('copy-selected')).toBeUndefined() + }) + }) + + describe('Accessibility', () => { + it('should have proper semantic structure', () => { + wrapper = mountComponent() + + expect(wrapper.find('div').exists()).toBe(true) + expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true) + expect(wrapper.find('button').exists()).toBe(true) + }) + + it('should have proper form controls', () => { + wrapper = mountComponent() + const checkbox = wrapper.find('input[type="checkbox"]') + const button = wrapper.find('button') + + expect(checkbox.attributes('type')).toBe('checkbox') + expect(button.text()).toBe('Copy') + }) + }) + + describe('Conditional Rendering', () => { + it('should show both controls when showGiveNumbers is false', () => { + wrapper = mountComponent({ showGiveNumbers: false }) + + expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true) + expect(wrapper.find('button').exists()).toBe(true) + }) + + it('should hide both controls when showGiveNumbers is true', () => { + wrapper = mountComponent({ showGiveNumbers: true }) + + expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false) + expect(wrapper.find('button').exists()).toBe(false) + }) + }) +}) \ No newline at end of file diff --git a/src/test/LargeIdenticonModal.test.ts b/src/test/LargeIdenticonModal.test.ts new file mode 100644 index 00000000..9e72fbe6 --- /dev/null +++ b/src/test/LargeIdenticonModal.test.ts @@ -0,0 +1,230 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import LargeIdenticonModal from '@/components/LargeIdenticonModal.vue' +import { Contact } from '@/db/tables/contacts' + +/** + * LargeIdenticonModal Component Tests + * + * Comprehensive test suite for the LargeIdenticonModal component. + * Tests component rendering, props, events, and user interactions. + * + * @author Matthew Raymer + */ +describe('LargeIdenticonModal', () => { + let wrapper: any + let mockContact: Contact + + /** + * Test setup - creates a fresh component instance before each test + */ + beforeEach(() => { + wrapper = null + mockContact = { + id: 1, + name: 'Test Contact', + did: 'did:ethr:test', + createdAt: new Date(), + updatedAt: new Date() + } as Contact + }) + + /** + * Helper function to mount component with props + * @param props - Component props + * @returns Vue test wrapper + */ + const mountComponent = (props = {}) => { + return mount(LargeIdenticonModal, { + props: { + contact: mockContact, + ...props + }, + global: { + stubs: { + EntityIcon: { + template: '
EntityIcon
', + props: ['contact', 'iconSize', 'class'] + } + } + } + }) + } + + describe('Component Rendering', () => { + it('should render when contact is provided', () => { + wrapper = mountComponent() + + expect(wrapper.exists()).toBe(true) + expect(wrapper.find('.fixed').exists()).toBe(true) + expect(wrapper.find('.absolute').exists()).toBe(true) + expect(wrapper.find('.entity-icon-stub').exists()).toBe(true) + }) + + it('should not render when contact is undefined', () => { + wrapper = mountComponent({ contact: undefined }) + + expect(wrapper.find('.fixed').exists()).toBe(false) + }) + + it('should not render when contact is null', () => { + wrapper = mountComponent({ contact: null }) + + expect(wrapper.find('.fixed').exists()).toBe(false) + }) + }) + + describe('Component Styling', () => { + it('should have correct modal CSS classes', () => { + wrapper = mountComponent() + const modal = wrapper.find('.fixed') + + expect(modal.classes()).toContain('fixed') + expect(modal.classes()).toContain('z-[100]') + expect(modal.classes()).toContain('top-0') + expect(modal.classes()).toContain('inset-x-0') + expect(modal.classes()).toContain('w-full') + }) + + it('should have correct overlay CSS classes', () => { + wrapper = mountComponent() + const overlay = wrapper.find('.absolute') + + expect(overlay.classes()).toContain('absolute') + expect(overlay.classes()).toContain('inset-0') + expect(overlay.classes()).toContain('h-screen') + expect(overlay.classes()).toContain('flex') + expect(overlay.classes()).toContain('flex-col') + expect(overlay.classes()).toContain('items-center') + expect(overlay.classes()).toContain('justify-center') + expect(overlay.classes()).toContain('bg-slate-900/50') + }) + + it('should have EntityIcon component', () => { + wrapper = mountComponent() + const entityIcon = wrapper.find('.entity-icon-stub') + + expect(entityIcon.exists()).toBe(true) + }) + }) + + describe('Component Props', () => { + it('should accept contact prop', () => { + wrapper = mountComponent() + expect(wrapper.vm.contact).toStrictEqual(mockContact) + }) + + it('should render EntityIcon component', () => { + wrapper = mountComponent() + const entityIcon = wrapper.find('.entity-icon-stub') + + expect(entityIcon.exists()).toBe(true) + }) + }) + + describe('User Interactions', () => { + it('should emit close event when EntityIcon is clicked', async () => { + wrapper = mountComponent() + const entityIcon = wrapper.find('.entity-icon-stub') + + await entityIcon.trigger('click') + + expect(wrapper.emitted('close')).toBeTruthy() + expect(wrapper.emitted('close')).toHaveLength(1) + }) + + it('should emit close event multiple times when clicked multiple times', async () => { + wrapper = mountComponent() + const entityIcon = wrapper.find('.entity-icon-stub') + + await entityIcon.trigger('click') + await entityIcon.trigger('click') + await entityIcon.trigger('click') + + expect(wrapper.emitted('close')).toBeTruthy() + expect(wrapper.emitted('close')).toHaveLength(3) + }) + }) + + describe('Component Methods', () => { + it('should have contact prop', () => { + wrapper = mountComponent() + expect(wrapper.vm.contact).toBeDefined() + }) + }) + + describe('Edge Cases', () => { + it('should handle rapid clicks efficiently', async () => { + wrapper = mountComponent() + const entityIcon = wrapper.find('.entity-icon-stub') + + // Simulate rapid clicks + await Promise.all([ + entityIcon.trigger('click'), + entityIcon.trigger('click'), + entityIcon.trigger('click') + ]) + + expect(wrapper.emitted('close')).toBeTruthy() + expect(wrapper.emitted('close')).toHaveLength(3) + }) + + it('should maintain component state after prop changes', async () => { + wrapper = mountComponent() + expect(wrapper.find('.fixed').exists()).toBe(true) + + await wrapper.setProps({ contact: undefined }) + expect(wrapper.find('.fixed').exists()).toBe(false) + + await wrapper.setProps({ contact: mockContact }) + expect(wrapper.find('.fixed').exists()).toBe(true) + }) + }) + + describe('Accessibility', () => { + it('should have proper semantic structure', () => { + wrapper = mountComponent() + + expect(wrapper.find('.fixed').exists()).toBe(true) + expect(wrapper.find('.absolute').exists()).toBe(true) + expect(wrapper.find('.entity-icon-stub').exists()).toBe(true) + }) + + it('should be clickable for closing', () => { + wrapper = mountComponent() + const entityIcon = wrapper.find('.entity-icon-stub') + + expect(entityIcon.exists()).toBe(true) + expect(entityIcon.isVisible()).toBe(true) + }) + }) + + describe('Modal Behavior', () => { + it('should cover full screen', () => { + wrapper = mountComponent() + const modal = wrapper.find('.fixed') + const overlay = wrapper.find('.absolute') + + expect(modal.classes()).toContain('inset-x-0') + expect(modal.classes()).toContain('w-full') + expect(overlay.classes()).toContain('inset-0') + expect(overlay.classes()).toContain('h-screen') + }) + + it('should have high z-index for overlay', () => { + wrapper = mountComponent() + const modal = wrapper.find('.fixed') + + expect(modal.classes()).toContain('z-[100]') + }) + + it('should center content', () => { + wrapper = mountComponent() + const overlay = wrapper.find('.absolute') + + expect(overlay.classes()).toContain('flex') + expect(overlay.classes()).toContain('items-center') + expect(overlay.classes()).toContain('justify-center') + }) + }) +}) \ No newline at end of file diff --git a/src/test/ProjectIcon.test.ts b/src/test/ProjectIcon.test.ts new file mode 100644 index 00000000..28ae9e6b --- /dev/null +++ b/src/test/ProjectIcon.test.ts @@ -0,0 +1,263 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import ProjectIcon from '@/components/ProjectIcon.vue' + +/** + * ProjectIcon Component Tests + * + * Comprehensive test suite for the ProjectIcon component. + * Tests component rendering, props, icon generation, and user interactions. + * + * @author Matthew Raymer + */ +describe('ProjectIcon', () => { + let wrapper: any + + /** + * Test setup - creates a fresh component instance before each test + */ + beforeEach(() => { + wrapper = null + }) + + /** + * Helper function to mount component with props + * @param props - Component props + * @returns Vue test wrapper + */ + const mountComponent = (props = {}) => { + return mount(ProjectIcon, { + props: { + entityId: 'test-entity', + iconSize: 64, + imageUrl: '', + linkToFullImage: false, + ...props + } + }) + } + + describe('Component Rendering', () => { + it('should render when all props are provided', () => { + wrapper = mountComponent() + + expect(wrapper.exists()).toBe(true) + expect(wrapper.find('div').exists()).toBe(true) + }) + + it('should render as link when linkToFullImage and imageUrl are provided', () => { + wrapper = mountComponent({ + imageUrl: 'test-image.jpg', + linkToFullImage: true + }) + + expect(wrapper.find('a').exists()).toBe(true) + expect(wrapper.find('a').attributes('href')).toBe('test-image.jpg') + expect(wrapper.find('a').attributes('target')).toBe('_blank') + }) + + it('should render as div when not a link', () => { + wrapper = mountComponent({ + imageUrl: 'test-image.jpg', + linkToFullImage: false + }) + + expect(wrapper.find('div').exists()).toBe(true) + expect(wrapper.find('a').exists()).toBe(false) + }) + + it('should render as div when no imageUrl', () => { + wrapper = mountComponent({ + imageUrl: '', + linkToFullImage: true + }) + + expect(wrapper.find('div').exists()).toBe(true) + expect(wrapper.find('a').exists()).toBe(false) + }) + }) + + describe('Component Styling', () => { + it('should have correct container CSS classes', () => { + wrapper = mountComponent() + const container = wrapper.find('div') + + expect(container.classes()).toContain('h-full') + expect(container.classes()).toContain('w-full') + expect(container.classes()).toContain('object-contain') + }) + + it('should have correct link CSS classes when rendered as link', () => { + wrapper = mountComponent({ + imageUrl: 'test-image.jpg', + linkToFullImage: true + }) + const link = wrapper.find('a') + + expect(link.classes()).toContain('h-full') + expect(link.classes()).toContain('w-full') + expect(link.classes()).toContain('object-contain') + }) + }) + + describe('Component Props', () => { + it('should accept entityId prop', () => { + wrapper = mountComponent({ entityId: 'test-entity-id' }) + expect(wrapper.vm.entityId).toBe('test-entity-id') + }) + + it('should accept iconSize prop', () => { + wrapper = mountComponent({ iconSize: 128 }) + expect(wrapper.vm.iconSize).toBe(128) + }) + + it('should accept imageUrl prop', () => { + wrapper = mountComponent({ imageUrl: 'test-image.png' }) + expect(wrapper.vm.imageUrl).toBe('test-image.png') + }) + + it('should accept linkToFullImage prop', () => { + wrapper = mountComponent({ linkToFullImage: true }) + expect(wrapper.vm.linkToFullImage).toBe(true) + }) + + it('should handle all props together', () => { + wrapper = mountComponent({ + entityId: 'test-entity', + iconSize: 64, + imageUrl: 'test-image.jpg', + linkToFullImage: true + }) + + expect(wrapper.vm.entityId).toBe('test-entity') + expect(wrapper.vm.iconSize).toBe(64) + expect(wrapper.vm.imageUrl).toBe('test-image.jpg') + expect(wrapper.vm.linkToFullImage).toBe(true) + }) + }) + + describe('Icon Generation', () => { + it('should generate image HTML when imageUrl is provided', () => { + wrapper = mountComponent({ imageUrl: 'test-image.jpg' }) + const generatedIcon = wrapper.vm.generateIcon() + + expect(generatedIcon).toContain(' { + wrapper = mountComponent({ imageUrl: '', iconSize: 64 }) + const generatedIcon = wrapper.vm.generateIcon() + + expect(generatedIcon).toContain(' { + wrapper = mountComponent({ entityId: '', iconSize: 64 }) + const generatedIcon = wrapper.vm.generateIcon() + + expect(generatedIcon).toContain(' { + it('should have generateIcon method', () => { + wrapper = mountComponent() + expect(typeof wrapper.vm.generateIcon).toBe('function') + }) + + it('should generate correct HTML for image', () => { + wrapper = mountComponent({ imageUrl: 'test-image.jpg' }) + const result = wrapper.vm.generateIcon() + + expect(result).toBe('') + }) + + it('should generate correct HTML for SVG', () => { + wrapper = mountComponent({ imageUrl: '', iconSize: 32 }) + const result = wrapper.vm.generateIcon() + + expect(result).toContain(' { + it('should handle empty entityId', () => { + wrapper = mountComponent({ entityId: '' }) + expect(wrapper.vm.entityId).toBe('') + }) + + it('should handle zero iconSize', () => { + wrapper = mountComponent({ iconSize: 0 }) + expect(wrapper.vm.iconSize).toBe(0) + }) + + it('should handle empty imageUrl', () => { + wrapper = mountComponent({ imageUrl: '' }) + expect(wrapper.vm.imageUrl).toBe('') + }) + + it('should handle false linkToFullImage', () => { + wrapper = mountComponent({ linkToFullImage: false }) + expect(wrapper.vm.linkToFullImage).toBe(false) + }) + + it('should maintain component state after prop changes', async () => { + wrapper = mountComponent({ imageUrl: '' }) + expect(wrapper.find('div').exists()).toBe(true) + + await wrapper.setProps({ imageUrl: 'test-image.jpg', linkToFullImage: true }) + expect(wrapper.find('a').exists()).toBe(true) + + await wrapper.setProps({ imageUrl: '' }) + expect(wrapper.find('div').exists()).toBe(true) + }) + }) + + describe('Accessibility', () => { + it('should have proper semantic structure when link', () => { + wrapper = mountComponent({ + imageUrl: 'test-image.jpg', + linkToFullImage: true + }) + + expect(wrapper.find('a').exists()).toBe(true) + expect(wrapper.find('a').attributes('target')).toBe('_blank') + }) + + it('should have proper semantic structure when div', () => { + wrapper = mountComponent() + + expect(wrapper.find('div').exists()).toBe(true) + }) + }) + + describe('Link Behavior', () => { + it('should open in new tab when link', () => { + wrapper = mountComponent({ + imageUrl: 'test-image.jpg', + linkToFullImage: true + }) + const link = wrapper.find('a') + + expect(link.attributes('target')).toBe('_blank') + }) + + it('should have correct href when link', () => { + wrapper = mountComponent({ + imageUrl: 'https://example.com/image.jpg', + linkToFullImage: true + }) + const link = wrapper.find('a') + + expect(link.attributes('href')).toBe('https://example.com/image.jpg') + }) + }) +}) \ No newline at end of file diff --git a/src/test/__mocks__/ContactBulkActions.mock.ts b/src/test/__mocks__/ContactBulkActions.mock.ts new file mode 100644 index 00000000..22e097d0 --- /dev/null +++ b/src/test/__mocks__/ContactBulkActions.mock.ts @@ -0,0 +1,82 @@ +import { Component, Vue, Prop } from "vue-facing-decorator"; + +/** + * ContactBulkActions Mock Component + * + * A mock implementation of the ContactBulkActions component for testing purposes. + * Provides the same interface as the original component but with simplified behavior + * for unit testing scenarios. + * + * @author Matthew Raymer + */ +@Component({ name: "ContactBulkActions" }) +export default class ContactBulkActionsMock extends Vue { + @Prop({ required: true }) showGiveNumbers!: boolean; + @Prop({ required: true }) allContactsSelected!: boolean; + @Prop({ required: true }) copyButtonClass!: string; + @Prop({ required: true }) copyButtonDisabled!: boolean; + + /** + * Mock method to check if checkbox should be visible + * @returns boolean - true if checkbox should be shown + */ + get shouldShowCheckbox(): boolean { + return !this.showGiveNumbers; + } + + /** + * Mock method to check if copy button should be visible + * @returns boolean - true if copy button should be shown + */ + get shouldShowCopyButton(): boolean { + return !this.showGiveNumbers; + } + + /** + * Mock method to get checkbox CSS classes + * @returns string - CSS classes for the checkbox + */ + get checkboxClasses(): string { + return "align-middle ml-2 h-6 w-6"; + } + + /** + * Mock method to get container CSS classes + * @returns string - CSS classes for the container + */ + get containerClasses(): string { + return "mt-2 w-full text-left"; + } + + /** + * Mock method to simulate toggle all selection event + * @returns void + */ + mockToggleAllSelection(): void { + this.$emit('toggle-all-selection'); + } + + /** + * Mock method to simulate copy selected event + * @returns void + */ + mockCopySelected(): void { + this.$emit('copy-selected'); + } + + /** + * Mock method to get button text + * @returns string - the button text + */ + get buttonText(): string { + return "Copy"; + } + + /** + * Mock method to get test ID for checkbox + * @returns string - the test ID + */ + get checkboxTestId(): string { + return "contactCheckAllBottom"; + } +} \ No newline at end of file diff --git a/src/test/__mocks__/LargeIdenticonModal.mock.ts b/src/test/__mocks__/LargeIdenticonModal.mock.ts new file mode 100644 index 00000000..884dc52d --- /dev/null +++ b/src/test/__mocks__/LargeIdenticonModal.mock.ts @@ -0,0 +1,64 @@ +import { Component, Vue, Prop } from "vue-facing-decorator"; +import { Contact } from "../../db/tables/contacts"; + +/** + * LargeIdenticonModal Mock Component + * + * A mock implementation of the LargeIdenticonModal component for testing purposes. + * Provides the same interface as the original component but with simplified behavior + * for unit testing scenarios. + * + * @author Matthew Raymer + */ +@Component({ name: "LargeIdenticonModal" }) +export default class LargeIdenticonModalMock extends Vue { + @Prop({ required: true }) contact!: Contact | undefined; + + /** + * Mock method to check if modal should be visible + * @returns boolean - true if modal should be shown + */ + get shouldShow(): boolean { + return !!this.contact; + } + + /** + * Mock method to get modal CSS classes + * @returns string - CSS classes for the modal container + */ + get modalClasses(): string { + return "fixed z-[100] top-0 inset-x-0 w-full"; + } + + /** + * Mock method to get overlay CSS classes + * @returns string - CSS classes for the overlay + */ + get overlayClasses(): string { + return "absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"; + } + + /** + * Mock method to get icon CSS classes + * @returns string - CSS classes for the icon container + */ + get iconClasses(): string { + return "flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"; + } + + /** + * Mock method to simulate close event + * @returns void + */ + mockClose(): void { + this.$emit('close'); + } + + /** + * Mock method to get icon size + * @returns number - the icon size (512) + */ + get iconSize(): number { + return 512; + } +} \ No newline at end of file diff --git a/src/test/__mocks__/ProjectIcon.mock.ts b/src/test/__mocks__/ProjectIcon.mock.ts new file mode 100644 index 00000000..ae3d8535 --- /dev/null +++ b/src/test/__mocks__/ProjectIcon.mock.ts @@ -0,0 +1,88 @@ +import { Component, Vue, Prop } from "vue-facing-decorator"; + +/** + * ProjectIcon Mock Component + * + * A mock implementation of the ProjectIcon component for testing purposes. + * Provides the same interface as the original component but with simplified behavior + * for unit testing scenarios. + * + * @author Matthew Raymer + */ +@Component({ name: "ProjectIcon" }) +export default class ProjectIconMock extends Vue { + @Prop entityId = ""; + @Prop iconSize = 0; + @Prop imageUrl = ""; + @Prop linkToFullImage = false; + + /** + * Mock method to check if component should show image + * @returns boolean - true if image should be displayed + */ + get shouldShowImage(): boolean { + return !!this.imageUrl; + } + + /** + * Mock method to check if component should be a link + * @returns boolean - true if component should be a link + */ + get shouldBeLink(): boolean { + return this.linkToFullImage && !!this.imageUrl; + } + + /** + * Mock method to get container CSS classes + * @returns string - CSS classes for the container + */ + get containerClasses(): string { + return "h-full w-full object-contain"; + } + + /** + * Mock method to get image CSS classes + * @returns string - CSS classes for the image + */ + get imageClasses(): string { + return "w-full h-full object-contain"; + } + + /** + * Mock method to generate icon HTML + * @returns string - HTML for the icon + */ + generateIcon(): string { + if (this.imageUrl) { + return ``; + } else { + return ``; + } + } + + /** + * Mock method to get blank config + * @returns object - Blank configuration for jdenticon + */ + get blankConfig() { + return { + lightness: { + color: [1.0, 1.0], + grayscale: [1.0, 1.0], + }, + saturation: { + color: 0.0, + grayscale: 0.0, + }, + backColor: "#0000", + }; + } + + /** + * Mock method to check if should use blank config + * @returns boolean - true if blank config should be used + */ + get shouldUseBlankConfig(): boolean { + return !this.entityId; + } +} \ No newline at end of file