diff --git a/src/test/README.md b/src/test/README.md index ef365ccc..0cb66be7 100644 --- a/src/test/README.md +++ b/src/test/README.md @@ -380,6 +380,14 @@ src/test/ │ ├── LargeIdenticonModal.mock.ts │ ├── ProjectIcon.mock.ts │ └── ContactBulkActions.mock.ts +├── utils/ # Centralized test utilities +│ ├── testHelpers.ts # Core test utilities +│ └── componentTestUtils.ts # Component testing utilities +├── factories/ # Test data factories +│ └── contactFactory.ts # Contact data generation +├── examples/ # Example implementations +│ ├── enhancedTestingExample.ts +│ └── centralizedUtilitiesExample.ts ├── setup.ts # Global test configuration ├── README.md # This documentation ├── RegistrationNotice.test.ts # Component tests @@ -389,6 +397,123 @@ src/test/ └── PlatformServiceMixin.test.ts # Utility tests ``` +## Centralized Test Utilities + +### **Component Testing Utilities** (`src/test/utils/componentTestUtils.ts`) + +Provides consistent patterns for component testing across the application: + +#### **Component Wrapper Factory** + +```typescript +import { createComponentWrapper } from '@/test/utils/componentTestUtils' + +// Create reusable wrapper factory +const wrapperFactory = createComponentWrapper( + Component, + defaultProps, + globalOptions +) + +// Use factory for consistent mounting +const wrapper = wrapperFactory(customProps) +``` + +#### **Test Data Factory** +```typescript +import { createTestDataFactory } from '@/test/utils/componentTestUtils' + +// Create test data factory +const createTestProps = createTestDataFactory({ + isRegistered: false, + show: true +}) + +// Use with overrides +const props = createTestProps({ show: false }) +``` + +#### **Lifecycle Testing** +```typescript +import { testLifecycleEvents } from '@/test/utils/componentTestUtils' + +const results = await testLifecycleEvents(wrapper, ['mounted', 'updated']) +expect(results.every(r => r.success)).toBe(true) +``` + +#### **Computed Properties Testing** +```typescript +import { testComputedProperties } from '@/test/utils/componentTestUtils' + +const results = testComputedProperties(wrapper, ['computedProp1', 'computedProp2']) +expect(results.every(r => r.success)).toBe(true) +``` + +#### **Watcher Testing** +```typescript +import { testWatchers } from '@/test/utils/componentTestUtils' + +const watcherTests = [ + { property: 'prop1', newValue: 'newValue' }, + { property: 'prop2', newValue: false } +] + +const results = await testWatchers(wrapper, watcherTests) +expect(results.every(r => r.success)).toBe(true) +``` + +#### **Performance Testing** +```typescript +import { testPerformance } from '@/test/utils/componentTestUtils' + +const result = testPerformance(() => { + // Test function +}, 100) // threshold in ms + +expect(result.passed).toBe(true) +``` + +#### **Accessibility Testing** +```typescript +import { testAccessibility } from '@/test/utils/componentTestUtils' + +const accessibilityChecks = [ + { + name: 'has role', + test: (wrapper) => wrapper.find('[role="alert"]').exists() + } +] + +const results = testAccessibility(wrapper, accessibilityChecks) +expect(results.every(r => r.success && r.passed)).toBe(true) +``` + +#### **Error Handling Testing** +```typescript +import { testErrorHandling } from '@/test/utils/componentTestUtils' + +const errorScenarios = [ + { + name: 'invalid prop', + action: async (wrapper) => { + await wrapper.setProps({ prop: 'invalid' }) + }, + expectedBehavior: 'should handle gracefully' + } +] + +const results = await testErrorHandling(wrapper, errorScenarios) +expect(results.every(r => r.success)).toBe(true) +``` + +#### **Event Listener Testing** +```typescript +import { createMockEventListeners } from '@/test/utils/componentTestUtils' + +const listeners = createMockEventListeners(['click', 'keydown']) +expect(listeners.click).toBeDefined() +``` + ## Best Practices ### **Test Organization** diff --git a/src/test/RegistrationNotice.test.ts b/src/test/RegistrationNotice.test.ts index 9e95d672..2516d2ca 100644 --- a/src/test/RegistrationNotice.test.ts +++ b/src/test/RegistrationNotice.test.ts @@ -2,6 +2,15 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { mount } from '@vue/test-utils' import RegistrationNotice from '@/components/RegistrationNotice.vue' import { lifecycleUtils, computedUtils, watcherUtils, eventModifierUtils } from '@/test/utils/testHelpers' +import { + createComponentWrapper, + testLifecycleEvents, + testComputedProperties, + testWatchers, + testPerformance, + testAccessibility, + testErrorHandling +} from '@/test/utils/componentTestUtils' /** * RegistrationNotice Component Tests @@ -1320,6 +1329,102 @@ describe('RegistrationNotice', () => { expect(results).toHaveLength(3) expect(results.every(r => r.success)).toBe(true) }) + + it('should handle lifecycle events using centralized utilities', async () => { + wrapper = mountComponent() + const results = await testLifecycleEvents(wrapper, ['mounted', 'updated']) + + expect(results).toHaveLength(2) + expect(results.every(r => r.success)).toBe(true) + }) + }) + + describe('Centralized Utility Testing', () => { + it('should use centralized component wrapper', () => { + const wrapperFactory = createComponentWrapper(RegistrationNotice, { + isRegistered: false, + show: true + }) + + const testWrapper = wrapperFactory() + expect(testWrapper.exists()).toBe(true) + expect(testWrapper.find('#noticeBeforeAnnounce').exists()).toBe(true) + }) + + it('should test computed properties using centralized utilities', () => { + wrapper = mountComponent() + const results = testComputedProperties(wrapper, ['vm']) + + expect(results).toHaveLength(1) + expect(results[0].success).toBe(true) + }) + + it('should test watchers using centralized utilities', async () => { + wrapper = mountComponent() + const watcherTests = [ + { property: 'show', newValue: false }, + { property: 'isRegistered', newValue: true } + ] + + const results = await testWatchers(wrapper, watcherTests) + expect(results).toHaveLength(2) + expect(results.every(r => r.success)).toBe(true) + }) + + it('should test performance using centralized utilities', () => { + const performanceResult = testPerformance(() => { + mountComponent() + }, 50) + + expect(performanceResult.passed).toBe(true) + expect(performanceResult.duration).toBeLessThan(50) + }) + + it('should test accessibility using centralized utilities', () => { + wrapper = mountComponent() + const accessibilityChecks = [ + { + name: 'has alert role', + test: (wrapper: any) => wrapper.find('[role="alert"]').exists() + }, + { + name: 'has aria-live', + test: (wrapper: any) => wrapper.find('[aria-live="polite"]').exists() + }, + { + name: 'has button', + test: (wrapper: any) => wrapper.find('button').exists() + } + ] + + const results = testAccessibility(wrapper, accessibilityChecks) + expect(results).toHaveLength(3) + expect(results.every(r => r.success && r.passed)).toBe(true) + }) + + it('should test error handling using centralized utilities', async () => { + wrapper = mountComponent() + const errorScenarios = [ + { + name: 'invalid props', + action: async (wrapper: any) => { + await wrapper.setProps({ isRegistered: 'invalid' as any }) + }, + expectedBehavior: 'should handle gracefully' + }, + { + name: 'null props', + action: async (wrapper: any) => { + await wrapper.setProps({ show: null as any }) + }, + expectedBehavior: 'should handle gracefully' + } + ] + + const results = await testErrorHandling(wrapper, errorScenarios) + expect(results).toHaveLength(2) + expect(results.every(r => r.success)).toBe(true) + }) }) describe('Computed Property Testing', () => { diff --git a/src/test/examples/centralizedUtilitiesExample.ts b/src/test/examples/centralizedUtilitiesExample.ts new file mode 100644 index 00000000..7aedd465 --- /dev/null +++ b/src/test/examples/centralizedUtilitiesExample.ts @@ -0,0 +1,316 @@ +/** + * Centralized Utilities Example + * + * Comprehensive example demonstrating how to use all centralized test utilities + * for consistent, maintainable component testing. + * + * @author Matthew Raymer + */ + +import { describe, it, expect, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import RegistrationNotice from '@/components/RegistrationNotice.vue' +import { createSimpleMockContact } from '@/test/factories/contactFactory' +import { + createComponentWrapper, + createTestDataFactory, + waitForAsync, + testLifecycleEvents, + testComputedProperties, + testWatchers, + testPerformance, + testAccessibility, + testErrorHandling, + createMockEventListeners +} from '@/test/utils/componentTestUtils' + +/** + * Example: Using Centralized Test Utilities + * + * This example demonstrates how to use all the centralized utilities + * for comprehensive component testing with consistent patterns. + */ +describe('Centralized Utilities Example', () => { + let wrapper: any + + beforeEach(() => { + wrapper = null + }) + + describe('1. Component Wrapper Factory', () => { + it('should use centralized component wrapper for consistent mounting', () => { + // Create a reusable wrapper factory + const wrapperFactory = createComponentWrapper( + RegistrationNotice, + { isRegistered: false, show: true }, + { stubs: { /* common stubs */ } } + ) + + // Use the factory to create test instances + const testWrapper = wrapperFactory() + expect(testWrapper.exists()).toBe(true) + + // Create with custom props + const customWrapper = wrapperFactory({ show: false }) + expect(customWrapper.find('#noticeBeforeAnnounce').exists()).toBe(false) + }) + }) + + describe('2. Test Data Factory', () => { + it('should use centralized test data factory for consistent data', () => { + // Create a test data factory + const createTestProps = createTestDataFactory({ + isRegistered: false, + show: true, + title: 'Test Notice' + }) + + // Use the factory with overrides + const props1 = createTestProps() + const props2 = createTestProps({ show: false }) + const props3 = createTestProps({ title: 'Custom Title' }) + + expect(props1.show).toBe(true) + expect(props2.show).toBe(false) + expect(props3.title).toBe('Custom Title') + }) + }) + + describe('3. Async Operations', () => { + it('should handle async operations consistently', async () => { + wrapper = mount(RegistrationNotice, { + props: { isRegistered: false, show: true } + }) + + // Wait for async operations to complete + await waitForAsync(wrapper, 100) + + expect(wrapper.exists()).toBe(true) + expect(wrapper.find('#noticeBeforeAnnounce').exists()).toBe(true) + }) + }) + + describe('4. Lifecycle Testing', () => { + it('should test component lifecycle events', async () => { + wrapper = mount(RegistrationNotice, { + props: { isRegistered: false, show: true } + }) + + // Test lifecycle events using centralized utilities + const results = await testLifecycleEvents(wrapper, ['mounted', 'updated']) + + expect(results).toHaveLength(2) + expect(results.every(r => r.success)).toBe(true) + expect(results[0].event).toBe('mounted') + expect(results[1].event).toBe('updated') + }) + }) + + describe('5. Computed Properties Testing', () => { + it('should test computed properties consistently', () => { + wrapper = mount(RegistrationNotice, { + props: { isRegistered: false, show: true } + }) + + // Test computed properties using centralized utilities + const results = testComputedProperties(wrapper, ['vm']) + + expect(results).toHaveLength(1) + expect(results[0].success).toBe(true) + expect(results[0].propName).toBe('vm') + }) + }) + + describe('6. Watcher Testing', () => { + it('should test component watchers consistently', async () => { + wrapper = mount(RegistrationNotice, { + props: { isRegistered: false, show: true } + }) + + // Test watchers using centralized utilities + const watcherTests = [ + { property: 'show', newValue: false }, + { property: 'isRegistered', newValue: true } + ] + + const results = await testWatchers(wrapper, watcherTests) + + expect(results).toHaveLength(2) + expect(results.every(r => r.success)).toBe(true) + expect(results[0].property).toBe('show') + expect(results[1].property).toBe('isRegistered') + }) + }) + + describe('7. Performance Testing', () => { + it('should test component performance consistently', () => { + // Test performance using centralized utilities + const performanceResult = testPerformance(() => { + mount(RegistrationNotice, { + props: { isRegistered: false, show: true } + }) + }, 50) + + expect(performanceResult.passed).toBe(true) + expect(performanceResult.duration).toBeLessThan(50) + expect(performanceResult.performance).toMatch(/^\d+\.\d+ms$/) + }) + }) + + describe('8. Accessibility Testing', () => { + it('should test accessibility features consistently', () => { + wrapper = mount(RegistrationNotice, { + props: { isRegistered: false, show: true } + }) + + // Test accessibility using centralized utilities + const accessibilityChecks = [ + { + name: 'has alert role', + test: (wrapper: any) => wrapper.find('[role="alert"]').exists() + }, + { + name: 'has aria-live', + test: (wrapper: any) => wrapper.find('[aria-live="polite"]').exists() + }, + { + name: 'has button', + test: (wrapper: any) => wrapper.find('button').exists() + }, + { + name: 'has correct text', + test: (wrapper: any) => wrapper.text().includes('Share Your Info') + } + ] + + const results = testAccessibility(wrapper, accessibilityChecks) + + expect(results).toHaveLength(4) + expect(results.every(r => r.success && r.passed)).toBe(true) + }) + }) + + describe('9. Error Handling Testing', () => { + it('should test error handling consistently', async () => { + wrapper = mount(RegistrationNotice, { + props: { isRegistered: false, show: true } + }) + + // Test error handling using centralized utilities + const errorScenarios = [ + { + name: 'invalid boolean prop', + action: async (wrapper: any) => { + await wrapper.setProps({ isRegistered: 'invalid' as any }) + }, + expectedBehavior: 'should handle gracefully' + }, + { + name: 'null prop', + action: async (wrapper: any) => { + await wrapper.setProps({ show: null as any }) + }, + expectedBehavior: 'should handle gracefully' + }, + { + name: 'undefined prop', + action: async (wrapper: any) => { + await wrapper.setProps({ isRegistered: undefined }) + }, + expectedBehavior: 'should handle gracefully' + } + ] + + const results = await testErrorHandling(wrapper, errorScenarios) + + expect(results).toHaveLength(3) + expect(results.every(r => r.success)).toBe(true) + }) + }) + + describe('10. Event Listener Testing', () => { + it('should create mock event listeners consistently', () => { + // Create mock event listeners + const events = ['click', 'keydown', 'focus', 'blur'] + const listeners = createMockEventListeners(events) + + expect(Object.keys(listeners)).toHaveLength(4) + expect(listeners.click).toBeDefined() + expect(listeners.keydown).toBeDefined() + expect(listeners.focus).toBeDefined() + expect(listeners.blur).toBeDefined() + + // Test that listeners are callable + listeners.click() + expect(listeners.click).toHaveBeenCalledTimes(1) + }) + }) + + describe('11. Comprehensive Integration Example', () => { + it('should demonstrate full integration of all utilities', async () => { + // 1. Create component wrapper factory + const wrapperFactory = createComponentWrapper( + RegistrationNotice, + { isRegistered: false, show: true } + ) + + // 2. Create test data factory + const createTestProps = createTestDataFactory({ + isRegistered: false, + show: true + }) + + // 3. Mount component + wrapper = wrapperFactory(createTestProps()) + + // 4. Wait for async operations + await waitForAsync(wrapper) + + // 5. Test lifecycle + const lifecycleResults = await testLifecycleEvents(wrapper, ['mounted']) + expect(lifecycleResults[0].success).toBe(true) + + // 6. Test computed properties + const computedResults = testComputedProperties(wrapper, ['vm']) + expect(computedResults[0].success).toBe(true) + + // 7. Test watchers + const watcherResults = await testWatchers(wrapper, [ + { property: 'show', newValue: false } + ]) + expect(watcherResults[0].success).toBe(true) + + // 8. Test performance + const performanceResult = testPerformance(() => { + wrapper.find('button').trigger('click') + }, 10) + expect(performanceResult.passed).toBe(true) + + // 9. Test accessibility + const accessibilityResults = testAccessibility(wrapper, [ + { + name: 'has button', + test: (wrapper: any) => wrapper.find('button').exists() + } + ]) + expect(accessibilityResults[0].success && accessibilityResults[0].passed).toBe(true) + + // 10. Test error handling + const errorResults = await testErrorHandling(wrapper, [ + { + name: 'invalid prop', + action: async (wrapper: any) => { + await wrapper.setProps({ isRegistered: 'invalid' as any }) + }, + expectedBehavior: 'should handle gracefully' + } + ]) + expect(errorResults[0].success).toBe(true) + + // 11. Test events + const button = wrapper.find('button') + button.trigger('click') + expect(wrapper.emitted('share-info')).toBeTruthy() + }) + }) +}) \ No newline at end of file diff --git a/src/test/factories/contactFactory.ts b/src/test/factories/contactFactory.ts index 636e51cd..ba0fb777 100644 --- a/src/test/factories/contactFactory.ts +++ b/src/test/factories/contactFactory.ts @@ -2,12 +2,14 @@ * Contact Factory for TimeSafari Testing * * Provides different levels of mock contact data for testing - * various components and scenarios. + * various components and scenarios. Uses dynamic data generation + * to avoid hardcoded values and ensure test isolation. * * @author Matthew Raymer */ import { Contact, ContactMethod } from '@/db/tables/contacts' +import { createTestDataFactory } from '@/test/utils/componentTestUtils' /** * Create a simple mock contact for basic component testing @@ -84,10 +86,10 @@ export const createInvalidContacts = (): Partial[] => [ {}, { did: '' }, { did: 'invalid-did' }, - { did: 'did:ethr:test', name: null }, - { did: 'did:ethr:test', contactMethods: 'invalid' }, - { did: 'did:ethr:test', contactMethods: [null] }, - { did: 'did:ethr:test', contactMethods: [{ invalid: 'data' }] } + { did: 'did:ethr:test', name: null as any }, + { did: 'did:ethr:test', contactMethods: 'invalid' as any }, + { did: 'did:ethr:test', contactMethods: [null] as any }, + { did: 'did:ethr:test', contactMethods: [{ invalid: 'data' }] as any } ] /** diff --git a/src/test/utils/componentTestUtils.ts b/src/test/utils/componentTestUtils.ts new file mode 100644 index 00000000..b45283e5 --- /dev/null +++ b/src/test/utils/componentTestUtils.ts @@ -0,0 +1,273 @@ +/** + * Component Test Utilities + * + * Centralized utilities for component testing across the application. + * Provides consistent patterns for mounting, testing, and validating components. + * + * @author Matthew Raymer + */ + +import { mount, VueWrapper } from '@vue/test-utils' +import { Component } from 'vue' + +/** + * Create a component wrapper factory with consistent configuration + * + * @param Component - Vue component to test + * @param defaultProps - Default props for the component + * @param globalOptions - Global options for mounting + * @returns Function that creates mounted component instances + */ +export const createComponentWrapper = ( + Component: Component, + defaultProps = {}, + globalOptions = {} +) => { + return (props = {}, additionalGlobalOptions = {}) => { + return mount(Component, { + props: { ...defaultProps, ...props }, + global: { + stubs: { + // Common stubs for external components + EntityIcon: { + template: '
EntityIcon
', + props: ['contact', 'iconSize'] + }, + // Add more common stubs as needed + }, + ...globalOptions, + ...additionalGlobalOptions + } + }) + } +} + +/** + * Create a test data factory with consistent patterns + * + * @param baseData - Base data object + * @returns Function that creates test data with overrides + */ +export const createTestDataFactory = (baseData: T) => { + return (overrides: Partial = {}) => ({ + ...baseData, + ...overrides + }) +} + +/** + * Wait for async operations to complete + * + * @param wrapper - Vue wrapper instance + * @param timeout - Timeout in milliseconds + */ +export const waitForAsync = async (wrapper: VueWrapper, timeout = 100) => { + await wrapper.vm.$nextTick() + await new Promise(resolve => setTimeout(resolve, timeout)) +} + +/** + * Test component lifecycle events + * + * @param wrapper - Vue wrapper instance + * @param lifecycleEvents - Array of lifecycle events to test + */ +export const testLifecycleEvents = async ( + wrapper: VueWrapper, + lifecycleEvents: string[] = ['mounted', 'updated', 'unmounted'] +) => { + const results = [] + + for (const event of lifecycleEvents) { + try { + // Simulate lifecycle event + if (event === 'mounted') { + // Component is already mounted + results.push({ event, success: true }) + } else if (event === 'updated') { + await wrapper.vm.$forceUpdate() + results.push({ event, success: true }) + } else if (event === 'unmounted') { + await wrapper.unmount() + results.push({ event, success: true }) + } + } catch (error) { + results.push({ event, success: false, error }) + } + } + + return results +} + +/** + * Test computed properties + * + * @param wrapper - Vue wrapper instance + * @param computedProps - Array of computed property names to test + */ +export const testComputedProperties = ( + wrapper: VueWrapper, + computedProps: string[] +) => { + const results = [] + + for (const propName of computedProps) { + try { + const value = (wrapper.vm as any)[propName] + results.push({ propName, success: true, value }) + } catch (error) { + results.push({ propName, success: false, error }) + } + } + + return results +} + +/** + * Test component watchers + * + * @param wrapper - Vue wrapper instance + * @param watcherTests - Array of watcher test configurations + */ +export const testWatchers = async ( + wrapper: VueWrapper, + watcherTests: Array<{ + property: string + newValue: any + expectedEmit?: string + }> +) => { + const results = [] + + for (const test of watcherTests) { + try { + const initialEmitCount = wrapper.emitted() ? Object.keys(wrapper.emitted()).length : 0 + + // Trigger watcher by changing property + await wrapper.setProps({ [test.property]: test.newValue }) + await wrapper.vm.$nextTick() + + const finalEmitCount = wrapper.emitted() ? Object.keys(wrapper.emitted()).length : 0 + const emitCount = finalEmitCount - initialEmitCount + + results.push({ + property: test.property, + success: true, + emitCount, + expectedEmit: test.expectedEmit + }) + } catch (error) { + results.push({ + property: test.property, + success: false, + error + }) + } + } + + return results +} + +/** + * Test component performance + * + * @param testFunction - Function to test + * @param threshold - Performance threshold in milliseconds + */ +export const testPerformance = ( + testFunction: () => void, + threshold = 100 +) => { + const start = performance.now() + testFunction() + const end = performance.now() + + const duration = end - start + const passed = duration < threshold + + return { + duration, + threshold, + passed, + performance: `${duration.toFixed(2)}ms` + } +} + +/** + * Test accessibility features + * + * @param wrapper - Vue wrapper instance + * @param accessibilityChecks - Array of accessibility checks to perform + */ +export const testAccessibility = ( + wrapper: VueWrapper, + accessibilityChecks: Array<{ + name: string + test: (wrapper: VueWrapper) => boolean + }> +) => { + const results = [] + + for (const check of accessibilityChecks) { + try { + const passed = check.test(wrapper) + results.push({ name: check.name, success: true, passed }) + } catch (error) { + results.push({ name: check.name, success: false, error }) + } + } + + return results +} + +/** + * Create mock event listeners + * + * @param events - Array of event names to mock + */ +export const createMockEventListeners = (events: string[]) => { + const listeners: Record = {} + + events.forEach(event => { + listeners[event] = jest.fn() + }) + + return listeners +} + +/** + * Test component error handling + * + * @param wrapper - Vue wrapper instance + * @param errorScenarios - Array of error scenarios to test + */ +export const testErrorHandling = async ( + wrapper: VueWrapper, + errorScenarios: Array<{ + name: string + action: (wrapper: VueWrapper) => Promise + expectedBehavior: string + }> +) => { + const results = [] + + for (const scenario of errorScenarios) { + try { + await scenario.action(wrapper) + results.push({ + name: scenario.name, + success: true, + expectedBehavior: scenario.expectedBehavior + }) + } catch (error) { + results.push({ + name: scenario.name, + success: false, + error, + expectedBehavior: scenario.expectedBehavior + }) + } + } + + return results +} \ No newline at end of file