diff --git a/src/test/ContactListItem.test.ts b/src/test/ContactListItem.test.ts new file mode 100644 index 00000000..3f57db0d --- /dev/null +++ b/src/test/ContactListItem.test.ts @@ -0,0 +1,517 @@ +/** + * ContactListItem Component Tests + * + * Comprehensive test suite for the ContactListItem component. + * Tests component rendering, props, events, and user interactions. + * + * @author Matthew Raymer + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import ContactListItem from '@/components/ContactListItem.vue' +import { createStandardMockContact } from '@/test/factories/contactFactory' +import { + createComponentWrapper, + testLifecycleEvents, + testPerformance, + testAccessibility, + testErrorHandling +} from '@/test/utils/componentTestUtils' + +describe('ContactListItem', () => { + let wrapper: any + + beforeEach(() => { + wrapper = null + }) + + const mountComponent = (props = {}) => { + return mount(ContactListItem, { + props: { + contact: createStandardMockContact(), + activeDid: 'did:ethr:test:active', + showCheckbox: false, + showActions: false, + isSelected: false, + showGiveTotals: true, + showGiveConfirmed: true, + givenToMeDescriptions: {}, + givenToMeConfirmed: {}, + givenToMeUnconfirmed: {}, + givenByMeDescriptions: {}, + givenByMeConfirmed: {}, + givenByMeUnconfirmed: {}, + ...props + }, + global: { + stubs: { + EntityIcon: { + template: '
', + props: ['contact', 'iconSize'] + }, + 'font-awesome': { + template: 'FontAwesome' + } + } + } + }) + } + + describe('Component Rendering', () => { + it('should render with correct structure when all props are provided', () => { + wrapper = mountComponent() + + expect(wrapper.exists()).toBe(true) + expect(wrapper.find('[data-testid="contactListItem"]').exists()).toBe(true) + expect(wrapper.find('.entity-icon-stub').exists()).toBe(true) + expect(wrapper.find('h2').exists()).toBe(true) + }) + + it('should display contact name correctly', () => { + const contact = createStandardMockContact({ name: 'Test Contact' }) + wrapper = mountComponent({ contact }) + + expect(wrapper.find('h2').text().replace(/\u00A0/g, ' ')).toContain('Test Contact') + }) + + it('should display contact DID correctly', () => { + const contact = createStandardMockContact({ did: 'did:ethr:test:123' }) + wrapper = mountComponent({ contact }) + + expect(wrapper.text()).toContain('did:ethr:test:123') + }) + + it('should display contact notes when available', () => { + const contact = createStandardMockContact({ notes: 'Test notes' }) + wrapper = mountComponent({ contact }) + + expect(wrapper.text()).toContain('Test notes') + }) + }) + + describe('Checkbox Functionality', () => { + it('should show checkbox when showCheckbox is true', () => { + wrapper = mountComponent({ showCheckbox: true }) + + expect(wrapper.find('[data-testid="contactCheckOne"]').exists()).toBe(true) + }) + + it('should not show checkbox when showCheckbox is false', () => { + wrapper = mountComponent({ showCheckbox: false }) + + expect(wrapper.find('[data-testid="contactCheckOne"]').exists()).toBe(false) + }) + + it('should emit toggle-selection event when checkbox is clicked', () => { + const contact = createStandardMockContact({ did: 'did:ethr:test:123' }) + wrapper = mountComponent({ showCheckbox: true, contact }) + + wrapper.find('[data-testid="contactCheckOne"]').trigger('click') + + expect(wrapper.emitted('toggle-selection')).toBeTruthy() + expect(wrapper.emitted('toggle-selection')[0]).toEqual(['did:ethr:test:123']) + }) + + it('should reflect isSelected prop in checkbox state', () => { + wrapper = mountComponent({ showCheckbox: true, isSelected: true }) + + const checkbox = wrapper.find('[data-testid="contactCheckOne"]') + expect(checkbox.attributes('checked')).toBeDefined() + }) + }) + + describe('Actions Section', () => { + it('should show actions when showActions is true and contact is not active', () => { + wrapper = mountComponent({ + showActions: true, + contact: createStandardMockContact({ did: 'did:ethr:test:other' }) + }) + + expect(wrapper.find('[data-testid="offerButton"]').exists()).toBe(true) + }) + + it('should not show actions when contact is active', () => { + const contact = createStandardMockContact({ did: 'did:ethr:test:active' }) + wrapper = mountComponent({ + showActions: true, + contact, + activeDid: 'did:ethr:test:active' + }) + + expect(wrapper.find('[data-testid="offerButton"]').exists()).toBe(false) + }) + + it('should emit show-identicon event when EntityIcon is clicked', () => { + const contact = createStandardMockContact() + wrapper = mountComponent({ contact }) + + wrapper.find('.entity-icon-stub').trigger('click') + + expect(wrapper.emitted('show-identicon')).toBeTruthy() + expect(wrapper.emitted('show-identicon')[0]).toEqual([contact]) + }) + + it('should emit open-offer-dialog event when offer button is clicked', () => { + const contact = createStandardMockContact({ + did: 'did:ethr:test:123', + name: 'Test Contact' + }) + wrapper = mountComponent({ + showActions: true, + contact: createStandardMockContact({ did: 'did:ethr:test:other' }) + }) + + wrapper.find('[data-testid="offerButton"]').trigger('click') + + expect(wrapper.emitted('open-offer-dialog')).toBeTruthy() + expect(wrapper.emitted('open-offer-dialog')[0][0]).toBe('did:ethr:test:other') + }) + }) + + describe('Give Amounts Display', () => { + it('should display give amounts correctly for given to me', () => { + const contact = createStandardMockContact({ did: 'did:ethr:test:123' }) + wrapper = mountComponent({ + contact, + showActions: true, + givenToMeConfirmed: { 'did:ethr:test:123': 50 }, + givenToMeUnconfirmed: { 'did:ethr:test:123': 25 } + }) + + const buttons = wrapper.findAll('button') + if (buttons.length > 0) { + expect(buttons[0].text()).toBe('75') // 50 + 25 + } + }) + + it('should display give amounts correctly for given by me', () => { + const contact = createStandardMockContact({ did: 'did:ethr:test:123' }) + wrapper = mountComponent({ + contact, + showActions: true, + givenByMeConfirmed: { 'did:ethr:test:123': 30 }, + givenByMeUnconfirmed: { 'did:ethr:test:123': 20 } + }) + + const buttons = wrapper.findAll('button') + if (buttons.length > 1) { + expect(buttons[1].text()).toBe('50') // 30 + 20 + } + }) + + it('should show only confirmed amounts when showGiveConfirmed is true', () => { + const contact = createStandardMockContact({ did: 'did:ethr:test:123' }) + wrapper = mountComponent({ + contact, + showActions: true, + showGiveTotals: false, + showGiveConfirmed: true, + givenToMeConfirmed: { 'did:ethr:test:123': 50 }, + givenToMeUnconfirmed: { 'did:ethr:test:123': 25 } + }) + + const buttons = wrapper.findAll('button') + if (buttons.length > 0) { + expect(buttons[0].text()).toBe('50') // Only confirmed + } + }) + + it('should show only unconfirmed amounts when showGiveConfirmed is false', () => { + const contact = createStandardMockContact({ did: 'did:ethr:test:123' }) + wrapper = mountComponent({ + contact, + showActions: true, + showGiveTotals: false, + showGiveConfirmed: false, + givenToMeConfirmed: { 'did:ethr:test:123': 50 }, + givenToMeUnconfirmed: { 'did:ethr:test:123': 25 } + }) + + const buttons = wrapper.findAll('button') + if (buttons.length > 0) { + expect(buttons[0].text()).toBe('25') // Only unconfirmed + } + }) + }) + + describe('Error Handling', () => { + it('should handle undefined contact name gracefully', () => { + const contact = createStandardMockContact({ name: undefined }) + wrapper = mountComponent({ contact }) + + expect(wrapper.find('h2').text().replace(/\u00A0/g, ' ')).toContain('(no name)') + }) + + it('should handle missing give amounts gracefully', () => { + const contact = createStandardMockContact({ did: 'did:ethr:test:123' }) + wrapper = mountComponent({ + contact, + showActions: true, + givenToMeConfirmed: {}, + givenToMeUnconfirmed: {}, + givenByMeConfirmed: {}, + givenByMeUnconfirmed: {} + }) + + const buttons = wrapper.findAll('button') + if (buttons.length > 0) { + expect(buttons[0].text()).toBe('0') + } + if (buttons.length > 1) { + expect(buttons[1].text()).toBe('0') + } + }) + + it('should handle rapid prop changes gracefully', () => { + wrapper = mountComponent() + + for (let i = 0; i < 10; i++) { + wrapper.setProps({ + isSelected: i % 2 === 0, + showCheckbox: i % 3 === 0, + showActions: i % 4 === 0 + }) + } + + expect(wrapper.exists()).toBe(true) + }) + }) + + describe('Performance Testing', () => { + it('should render within performance threshold', () => { + const performanceResult = testPerformance(() => { + mountComponent() + }, 50) + + expect(performanceResult.passed).toBe(true) + expect(performanceResult.duration).toBeLessThan(50) + }) + + it('should handle multiple re-renders efficiently', () => { + wrapper = mountComponent() + + const start = performance.now() + for (let i = 0; i < 50; i++) { + wrapper.setProps({ isSelected: i % 2 === 0 }) + } + const end = performance.now() + + expect(end - start).toBeLessThan(200) + }) + + it('should establish performance baseline', () => { + const start = performance.now() + wrapper = mountComponent() + const end = performance.now() + + console.log('Performance Baseline:', { + renderTime: end - start + }) + + expect(end - start).toBeLessThan(100) + }) + }) + + describe('Integration Testing', () => { + it('should integrate with EntityIcon component correctly', () => { + const contact = createStandardMockContact() + wrapper = mountComponent({ contact }) + + const entityIcon = wrapper.find('.entity-icon-stub') + expect(entityIcon.exists()).toBe(true) + }) + + it('should handle multiple concurrent events', () => { + wrapper = mountComponent({ showCheckbox: true, showActions: true }) + + // Simulate multiple rapid interactions + wrapper.find('[data-testid="contactCheckOne"]').trigger('click') + wrapper.find('.entity-icon-stub').trigger('click') + wrapper.find('[data-testid="offerButton"]').trigger('click') + + expect(wrapper.emitted('toggle-selection')).toBeTruthy() + expect(wrapper.emitted('show-identicon')).toBeTruthy() + expect(wrapper.emitted('open-offer-dialog')).toBeTruthy() + }) + }) + + describe('Snapshot Testing', () => { + it('should maintain consistent DOM structure', () => { + wrapper = mountComponent() + const html = wrapper.html() + + expect(html).toMatch(/