Browse Source

feat: enhance snapshot testing for ProjectIcon and ContactBulkActions

Apply comprehensive snapshot testing improvements to ProjectIcon and ContactBulkActions components, matching the enhanced validation pattern established for RegistrationNotice and LargeIdenticonModal.

- ProjectIcon: Add specific structure validation with regex patterns, conditional rendering tests for different prop combinations (imageUrl, linkToFullImage), accessibility structure validation, and SVG structure verification
- ContactBulkActions: Add specific structure validation with regex patterns, conditional rendering tests for showGiveNumbers prop, accessibility attribute validation, and form control verification
- Fix conditional rendering logic to properly test Vue v-if behavior for both components
- Add comprehensive prop combination testing covering all rendering scenarios
- Maintain accessibility attribute validation where implemented (data-testid, SVG xmlns)

Improves component reliability with realistic validation that matches actual component structure and behavior, ensuring consistent testing quality across all simple components.
pull/153/head
Matthew Raymer 3 weeks ago
parent
commit
34df849398
  1. 58
      src/test/ContactBulkActions.test.ts
  2. 54
      src/test/LargeIdenticonModal.test.ts
  3. 51
      src/test/ProjectIcon.test.ts
  4. 99
      src/test/README.md
  5. 64
      src/test/RegistrationNotice.test.ts

58
src/test/ContactBulkActions.test.ts

@ -493,13 +493,65 @@ describe('ContactBulkActions', () => {
wrapper = mountComponent()
const html = wrapper.html()
// Basic structure validation
expect(html).toContain('<div')
expect(html).toContain('class="mt-2 w-full text-left"')
// Validate specific structure with regex patterns
expect(html).toMatch(/<div[^>]*class="[^"]*mt-2[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*w-full[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*text-left[^"]*"[^>]*>/)
expect(html).toMatch(/<input[^>]*type="checkbox"[^>]*>/)
expect(html).toMatch(/<button[^>]*class="[^"]*[^"]*"[^>]*>/)
// Validate accessibility attributes
expect(html).toContain('data-testid="contactCheckAllBottom"')
expect(html).toContain('Copy')
})
it('should maintain consistent structure with different prop combinations', () => {
const testCases = [
{ showGiveNumbers: false, allContactsSelected: true, copyButtonClass: 'btn-primary', copyButtonDisabled: false },
{ showGiveNumbers: false, allContactsSelected: false, copyButtonClass: 'btn-secondary', copyButtonDisabled: true },
{ showGiveNumbers: true, allContactsSelected: false, copyButtonClass: 'btn-primary', copyButtonDisabled: false }
]
testCases.forEach(props => {
const testWrapper = mountComponent(props)
const html = testWrapper.html()
if (!props.showGiveNumbers) {
// Should render checkbox and button
expect(html).toMatch(/<input[^>]*type="checkbox"[^>]*>/)
expect(html).toMatch(/<button[^>]*class="[^"]*[^"]*"[^>]*>/)
expect(html).toContain('Copy')
expect(html).toContain('data-testid="contactCheckAllBottom"')
} else {
// Should render outer div but inner elements are conditionally rendered
expect(html).toMatch(/<div[^>]*class="[^"]*mt-2[^"]*"[^>]*>/)
expect(html).not.toContain('<input')
expect(html).not.toContain('<button')
expect(html).not.toContain('Copy')
}
})
})
it('should maintain accessibility attributes consistently', () => {
wrapper = mountComponent()
const html = wrapper.html()
// Validate accessibility attributes
expect(html).toContain('data-testid="contactCheckAllBottom"')
// Validate semantic structure
expect(html).toMatch(/<div[^>]*class="[^"]*mt-2[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*w-full[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*text-left[^"]*"[^>]*>/)
// Validate form controls
const checkbox = wrapper.find('input[type="checkbox"]')
const button = wrapper.find('button')
expect(checkbox.exists()).toBe(true)
expect(button.exists()).toBe(true)
expect(checkbox.attributes('data-testid')).toBe('contactCheckAllBottom')
})
it('should have consistent CSS classes', () => {
wrapper = mountComponent()
const container = wrapper.find('.mt-2')

54
src/test/LargeIdenticonModal.test.ts

@ -746,11 +746,57 @@ describe('LargeIdenticonModal', () => {
wrapper = mountComponent()
const html = wrapper.html()
// Basic structure validation
expect(html).toContain('<div')
expect(html).toContain('class="fixed z-[100] top-0 inset-x-0 w-full"')
expect(html).toContain('class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"')
// Validate specific structure with regex patterns
expect(html).toMatch(/<div[^>]*class="[^"]*fixed[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*z-\[100\][^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*absolute[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*bg-slate-900\/50[^"]*"[^>]*>/)
// Validate EntityIcon component
expect(html).toContain('EntityIcon')
expect(html).toContain('entity-icon-stub')
// Note: Component doesn't have role="dialog" or aria-modal attributes
// These are not present in the actual component template
})
it('should maintain consistent structure with different contact states', () => {
const testCases = [
{ contact: mockContact },
{ contact: createSimpleMockContact({ name: 'Test Contact' }) },
{ contact: createSimpleMockContact({ id: 'complex-123', name: 'Complex Contact' }) }
]
testCases.forEach(props => {
const testWrapper = mountComponent(props)
const html = testWrapper.html()
// Core modal structure should always be present
expect(html).toMatch(/<div[^>]*class="[^"]*fixed[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*absolute[^"]*"[^>]*>/)
// EntityIcon should always be present when contact exists
expect(html).toContain('EntityIcon')
expect(html).toContain('entity-icon-stub')
})
})
it('should maintain accessibility attributes consistently', () => {
wrapper = mountComponent()
const html = wrapper.html()
// Note: Component doesn't have ARIA attributes in template
// These would need to be added to the component for accessibility
// Validate semantic structure
expect(html).toMatch(/<div[^>]*class="[^"]*fixed[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*absolute[^"]*"[^>]*>/)
// Validate modal positioning
const modal = wrapper.find('.fixed')
expect(modal.exists()).toBe(true)
expect(modal.classes()).toContain('z-[100]')
expect(modal.classes()).toContain('top-0')
})
it('should have consistent CSS classes', () => {

51
src/test/ProjectIcon.test.ts

@ -432,9 +432,54 @@ describe('ProjectIcon', () => {
wrapper = mountComponent()
const html = wrapper.html()
// Basic structure validation
expect(html).toContain('<div')
expect(html).toContain('class="h-full w-full object-contain"')
// Validate specific structure with regex patterns
expect(html).toMatch(/<div[^>]*class="[^"]*h-full[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*w-full[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*object-contain[^"]*"[^>]*>/)
// Validate SVG structure when no imageUrl
expect(html).toContain('<svg')
expect(html).toContain('xmlns="http://www.w3.org/2000/svg"')
})
it('should maintain consistent structure with different prop combinations', () => {
const testCases = [
{ entityId: 'test', iconSize: 64, imageUrl: '', linkToFullImage: false },
{ entityId: 'test', iconSize: 64, imageUrl: 'https://example.com/image.jpg', linkToFullImage: true },
{ entityId: '', iconSize: 64, imageUrl: '', linkToFullImage: false }
]
testCases.forEach(props => {
const testWrapper = mountComponent(props)
const html = testWrapper.html()
// Core structure should always be present
expect(html).toMatch(/<div[^>]*class="[^"]*h-full[^"]*"[^>]*>/)
if (props.imageUrl && props.linkToFullImage) {
// Should render as link with image
expect(html).toMatch(/<a[^>]*href="[^"]*"[^>]*>/)
expect(html).toMatch(/<img[^>]*src="[^"]*"[^>]*>/)
} else if (props.imageUrl) {
// Should render image without link
expect(html).toMatch(/<img[^>]*src="[^"]*"[^>]*>/)
} else {
// Should render SVG
expect(html).toContain('<svg')
}
})
})
it('should maintain accessibility structure consistently', () => {
wrapper = mountComponent()
const html = wrapper.html()
// Validate semantic structure
expect(html).toMatch(/<div[^>]*class="[^"]*h-full[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*w-full[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*object-contain[^"]*"[^>]*>/)
// Validate SVG accessibility
expect(html).toContain('<svg')
expect(html).toContain('xmlns="http://www.w3.org/2000/svg"')
})

99
src/test/README.md

@ -21,6 +21,7 @@ This directory contains comprehensive unit tests for TimeSafari components using
*EntityIcon.vue has 100% coverage but no dedicated test file (covered by LargeIdenticonModal tests)
### 📊 **Coverage Metrics**
- **Total Tests**: 149 tests passing
- **Test Files**: 5 files
- **Components Covered**: 5 simple components
@ -32,6 +33,7 @@ This directory contains comprehensive unit tests for TimeSafari components using
## Testing Infrastructure
### **Core Technologies**
- **Vitest**: Fast unit testing framework
- **JSDOM**: Browser-like environment for Node.js
- **@vue/test-utils**: Vue component testing utilities
@ -151,6 +153,103 @@ it('should have correct CSS classes', () => {
- Accessibility attribute consistency
- Visual structure verification
## Testing Philosophy
### **Defensive Programming Validation**
The primary purpose of our comprehensive error handling tests is to **prevent component and system failures** in real-world scenarios. Our testing philosophy focuses on:
#### **1. Real-World Edge Case Protection**
- **Invalid API responses**: Test components when backend returns `null` instead of expected objects
- **Network failures**: Verify graceful handling of missing or corrupted data
- **User input errors**: Test with malformed data, special characters, and extreme values
- **Concurrent operations**: Ensure stability during rapid state changes and simultaneous interactions
#### **2. System Stability Assurance**
- **Cascading failures**: Prevent one component's error from breaking the entire application
- **Memory leaks**: Ensure components clean up properly even when errors occur
- **Performance degradation**: Verify components remain responsive under error conditions
#### **3. Production Readiness**
- **User Experience Protection**: Users don't see blank screens or error messages
- **Developer Confidence**: Safe refactoring without fear of breaking edge cases
- **System Reliability**: Prevents one bad API response from crashing the entire app
### **Comprehensive Error Scenarios**
Our error handling tests cover:
#### **RegistrationNotice Component Protection**
- Prevents crashes when `isRegistered` or `show` props are malformed
- Ensures the "Share Your Info" button still works even with invalid data
- Protects against rapid prop changes causing UI inconsistencies
#### **LargeIdenticonModal Component Protection**
- Prevents modal rendering with invalid contact data that could break the UI
- Ensures the close functionality works even with malformed contact objects
- Protects against EntityIcon component failures cascading to the modal
### **Error Testing Categories**
#### **Invalid Input Testing**
```typescript
// Test 10+ different invalid prop combinations
const invalidPropCombinations = [
null, undefined, 'invalid', 0, -1, {}, [],
() => {}, NaN, Infinity
]
```
#### **Malformed Data Testing**
```typescript
// Test various malformed data structures
const malformedData = [
{ id: 'invalid' }, { name: null },
{ id: 0, name: '' }, { id: NaN, name: NaN }
]
```
#### **Extreme Value Testing**
```typescript
// Test boundary conditions and extreme values
const extremeValues = [
Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER,
Infinity, NaN, '', '\t\n\r'
]
```
#### **Concurrent Error Testing**
```typescript
// Test rapid changes with invalid data
for (let i = 0; i < 50; i++) {
await wrapper.setProps({
contact: i % 2 === 0 ? null : malformedContact
})
}
```
### **Benefits Beyond Coverage**
#### **1. Defensive Programming Validation**
- Components handle unexpected data gracefully
- No crashes or blank screens for users
- Proper error boundaries and fallbacks
#### **2. Real-World Resilience**
- Tested against actual failure scenarios
- Validated with realistic error conditions
- Proven stability under adverse conditions
#### **3. Developer Confidence**
- Safe to refactor and extend components
- Clear understanding of component behavior under stress
- Reduced debugging time for edge cases
#### **4. Production Stability**
- Reduced support tickets and user complaints
- Improved application reliability
- Better user experience under error conditions
## Mock Implementation
### **Mock Component Structure**

64
src/test/RegistrationNotice.test.ts

@ -1118,13 +1118,69 @@ describe('RegistrationNotice', () => {
wrapper = mountComponent()
const html = wrapper.html()
// Basic structure validation
expect(html).toContain('<div')
expect(html).toContain('id="noticeBeforeAnnounce"')
// Validate specific structure with regex patterns
expect(html).toMatch(/<div[^>]*id="noticeBeforeAnnounce"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*bg-amber-200[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*text-amber-900[^"]*"[^>]*>/)
// Validate button structure (actual classes from component)
expect(html).toMatch(/<button[^>]*class="[^"]*bg-gradient-to-b[^"]*"[^>]*>/)
expect(html).toMatch(/<button[^>]*class="[^"]*from-blue-400[^"]*"[^>]*>/)
expect(html).toMatch(/<button[^>]*class="[^"]*to-blue-700[^"]*"[^>]*>/)
expect(html).toMatch(/<button[^>]*class="[^"]*text-white[^"]*"[^>]*>/)
expect(html).toContain('Share Your Info')
// Validate accessibility structure
expect(html).toContain('role="alert"')
expect(html).toContain('aria-live="polite"')
expect(html).toContain('<button')
// Validate responsive design classes
expect(html).toMatch(/class="[^"]*px-4[^"]*"/)
expect(html).toMatch(/class="[^"]*py-3[^"]*"/)
expect(html).toMatch(/class="[^"]*rounded-md[^"]*"/)
})
it('should maintain consistent structure with different prop combinations', () => {
const testCases = [
{ isRegistered: true, show: true },
{ isRegistered: false, show: true },
{ isRegistered: true, show: false },
{ isRegistered: false, show: false }
]
testCases.forEach(props => {
const testWrapper = mountComponent(props)
const html = testWrapper.html()
// Component only renders when !isRegistered && show
if (!props.isRegistered && props.show) {
// Core structure should be present
expect(html).toMatch(/<div[^>]*id="noticeBeforeAnnounce"[^>]*>/)
expect(html).toMatch(/<button[^>]*class="[^"]*bg-gradient-to-b[^"]*"[^>]*>/)
expect(html).toContain('Share Your Info')
} else {
// Component should not render (v-if="!isRegistered && show")
expect(html).toBe('<!--v-if-->')
}
})
})
it('should maintain accessibility attributes consistently', () => {
wrapper = mountComponent()
const html = wrapper.html()
// Validate ARIA attributes
expect(html).toContain('role="alert"')
expect(html).toContain('aria-live="polite"')
// Validate semantic structure
expect(html).toMatch(/<div[^>]*class="[^"]*bg-amber-200[^"]*"[^>]*>/)
expect(html).toMatch(/<div[^>]*class="[^"]*text-amber-900[^"]*"[^>]*>/)
// Validate button accessibility (button doesn't have type attribute in component)
const button = wrapper.find('button')
expect(button.exists()).toBe(true)
// Note: Component doesn't specify type="button", so we don't test for it
})
it('should have consistent CSS classes', () => {

Loading…
Cancel
Save