Compare commits
1 Commits
units-mock
...
dialog-sty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f5111d100 |
@@ -204,4 +204,3 @@ Follow this exact order **after** the Base Contract’s **Objective → Result
|
|||||||
- Prefer clarity over completeness when timeboxed; capture unknowns explicitly.
|
- Prefer clarity over completeness when timeboxed; capture unknowns explicitly.
|
||||||
- Apply historical comment management rules (see `.cursor/rules/historical_comment_management.mdc`)
|
- Apply historical comment management rules (see `.cursor/rules/historical_comment_management.mdc`)
|
||||||
- Apply realistic time estimation rules (see `.cursor/rules/realistic_time_estimation.mdc`)
|
- Apply realistic time estimation rules (see `.cursor/rules/realistic_time_estimation.mdc`)
|
||||||
- Apply Playwright test investigation rules (see `.cursor/rules/playwright_test_investigation.mdc`)
|
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
---
|
|
||||||
description: when working with playwright tests either generating them or using them to test code
|
|
||||||
alwaysApply: false
|
|
||||||
---
|
|
||||||
# Playwright Test Investigation — Harbor Pilot Directive
|
|
||||||
|
|
||||||
**Author**: Matthew Raymer
|
|
||||||
**Date**: 2025-08-21T14:22Z
|
|
||||||
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
|
|
||||||
|
|
||||||
## Context & Scope
|
|
||||||
- **Audience**: Developers debugging Playwright test failures
|
|
||||||
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
|
|
||||||
- **Out of scope**: Test writing best practices, CI/CD configuration
|
|
||||||
|
|
||||||
## Artifacts & Links
|
|
||||||
- Test results: `test-results/` directory
|
|
||||||
- Error context: `error-context.md` files with page snapshots
|
|
||||||
- Trace files: `trace.zip` files for failed tests
|
|
||||||
- HTML reports: Interactive test reports with screenshots
|
|
||||||
|
|
||||||
## Environment & Preconditions
|
|
||||||
- OS/Runtime: Linux/Windows/macOS with Node.js
|
|
||||||
- Versions: Playwright test framework, browser drivers
|
|
||||||
- Services: Local test server (localhost:8080), test data setup
|
|
||||||
- Auth mode: None required for test investigation
|
|
||||||
|
|
||||||
## Architecture / Process Overview
|
|
||||||
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A[Test Failure] --> B[Check Error Context]
|
|
||||||
B --> C[Analyze Page Snapshot]
|
|
||||||
C --> D[Identify UI Conflicts]
|
|
||||||
D --> E[Check Trace Files]
|
|
||||||
E --> F[Verify Selector Uniqueness]
|
|
||||||
F --> G[Test Selector Fixes]
|
|
||||||
G --> H[Document Root Cause]
|
|
||||||
|
|
||||||
B --> I[Check Test Results Directory]
|
|
||||||
I --> J[Locate Failed Test Results]
|
|
||||||
J --> K[Extract Error Details]
|
|
||||||
|
|
||||||
D --> L[Multiple Alerts?]
|
|
||||||
L --> M[Button Text Conflicts?]
|
|
||||||
M --> N[Timing Issues?]
|
|
||||||
|
|
||||||
E --> O[Use Trace Viewer]
|
|
||||||
O --> P[Analyze Action Sequence]
|
|
||||||
P --> Q[Identify Failure Point]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Interfaces & Contracts
|
|
||||||
|
|
||||||
### Test Results Structure
|
|
||||||
| Component | Format | Content | Validation |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
|
|
||||||
| Trace Files | ZIP archive | Detailed execution trace | Use `npx playwright show-trace` |
|
|
||||||
| HTML Reports | Interactive HTML | Screenshots, traces, logs | Check browser for full report |
|
|
||||||
| JSON Results | JSON | Machine-readable results | Parse for automated analysis |
|
|
||||||
|
|
||||||
### Investigation Commands
|
|
||||||
| Step | Command | Expected Output | Notes |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
|
|
||||||
| Check error context | `cat test-results/*/error-context.md` | Page snapshots | Look for UI state conflicts |
|
|
||||||
| View traces | `npx playwright show-trace trace.zip` | Interactive trace viewer | Analyze exact failure sequence |
|
|
||||||
|
|
||||||
## Repro: End-to-End Investigation Procedure
|
|
||||||
|
|
||||||
### 1. Locate Failed Test Results
|
|
||||||
```bash
|
|
||||||
# Find all results for a specific test
|
|
||||||
find test-results -name "*test-name*" -type d
|
|
||||||
|
|
||||||
# Check for error context files
|
|
||||||
find test-results -name "error-context.md" | head -5
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Analyze Error Context
|
|
||||||
```bash
|
|
||||||
# Read error context for specific test
|
|
||||||
cat test-results/test-name-test-description-browser/error-context.md
|
|
||||||
|
|
||||||
# Look for UI conflicts in page snapshot
|
|
||||||
grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Check Trace Files
|
|
||||||
```bash
|
|
||||||
# List available trace files
|
|
||||||
find test-results -name "*.zip" | grep trace
|
|
||||||
|
|
||||||
# View trace in browser
|
|
||||||
npx playwright show-trace test-results/test-name/trace.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Investigate Selector Issues
|
|
||||||
```typescript
|
|
||||||
// Check for multiple elements with same text
|
|
||||||
await page.locator('button:has-text("Yes")').count(); // Should be 1
|
|
||||||
|
|
||||||
// Use more specific selectors
|
|
||||||
await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes")').click();
|
|
||||||
```
|
|
||||||
|
|
||||||
## What Works (Evidence)
|
|
||||||
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure
|
|
||||||
- **Time**: 2025-08-21T14:22Z
|
|
||||||
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
|
|
||||||
- **Verify at**: Error context files in test results directory
|
|
||||||
|
|
||||||
- ✅ **Trace files** capture detailed execution sequence for failed tests
|
|
||||||
- **Time**: 2025-08-21T14:22Z
|
|
||||||
- **Evidence**: `trace.zip` files available for all failed tests
|
|
||||||
- **Verify at**: Use `npx playwright show-trace <filename>`
|
|
||||||
|
|
||||||
- ✅ **Page snapshots** reveal UI conflicts like multiple alerts with duplicate button text
|
|
||||||
- **Time**: 2025-08-21T14:22Z
|
|
||||||
- **Evidence**: YAML snapshots show registration + export alerts simultaneously
|
|
||||||
- **Verify at**: Error context markdown files
|
|
||||||
|
|
||||||
## What Doesn't (Evidence & Hypotheses)
|
|
||||||
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
|
|
||||||
- **Time**: 2025-08-21T14:22Z
|
|
||||||
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
|
|
||||||
- **Hypothesis**: Selector ambiguity due to multiple alerts with conflicting button text
|
|
||||||
- **Next probe**: Use more specific selectors or dismiss alerts sequentially
|
|
||||||
|
|
||||||
- ❌ **Timing-dependent tests** fail due to alert stacking at `src/views/ContactsView.vue:860,1283`
|
|
||||||
- **Time**: 2025-08-21T14:22Z
|
|
||||||
- **Evidence**: Both alerts use identical 1000ms delays, ensuring simultaneous display
|
|
||||||
- **Hypothesis**: Race condition between alert displays creates UI conflicts
|
|
||||||
- **Next probe**: Implement alert queuing or prevent overlapping alerts
|
|
||||||
|
|
||||||
## Risks, Limits, Assumptions
|
|
||||||
- **Trace file size**: Large trace files may impact storage and analysis time
|
|
||||||
- **Browser compatibility**: Trace viewer requires specific browser support
|
|
||||||
- **Test isolation**: Shared state between tests may affect investigation results
|
|
||||||
- **Timing sensitivity**: Tests may pass/fail based on system performance
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
|
|
||||||
| Development Team | Implement alert queuing system | No overlapping alerts with conflicting buttons | 2025-08-25 |
|
|
||||||
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
|
|
||||||
|
|
||||||
## References
|
|
||||||
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
|
|
||||||
- [Playwright Test Results](https://playwright.dev/docs/test-reporters)
|
|
||||||
- [Test Investigation Workflow](./research_diagnostic.mdc)
|
|
||||||
|
|
||||||
## Competence Hooks
|
|
||||||
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes
|
|
||||||
- **Common pitfalls**: Generic selectors fail with multiple similar elements; timing issues create race conditions; alert stacking causes UI conflicts
|
|
||||||
- **Next skill unlock**: Implement unique test IDs and handle alert dismissal order in test flows
|
|
||||||
- **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
|
|
||||||
|
|
||||||
## Collaboration Hooks
|
|
||||||
- **Reviewers**: QA team, test automation engineers
|
|
||||||
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
|
|
||||||
|
|
||||||
## Assumptions & Limits
|
|
||||||
- Test results directory structure follows Playwright conventions
|
|
||||||
- Trace files are enabled in configuration (`trace: "retain-on-failure"`)
|
|
||||||
- Error context files contain valid YAML page snapshots
|
|
||||||
- Browser environment supports trace viewer functionality
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status**: Active investigation directive
|
|
||||||
**Priority**: High
|
|
||||||
**Maintainer**: Development team
|
|
||||||
**Next Review**: 2025-09-21
|
|
||||||
# Playwright Test Investigation — Harbor Pilot Directive
|
|
||||||
|
|
||||||
**Author**: Matthew Raymer
|
|
||||||
**Date**: 2025-08-21T14:22Z
|
|
||||||
**Status**: 🎯 **ACTIVE** - Playwright test debugging guidelines
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
Provide systematic approach for investigating Playwright test failures with focus on UI element conflicts, timing issues, and selector ambiguity.
|
|
||||||
|
|
||||||
## Context & Scope
|
|
||||||
- **Audience**: Developers debugging Playwright test failures
|
|
||||||
- **In scope**: Test failure analysis, selector conflicts, UI state investigation, timing issues
|
|
||||||
- **Out of scope**: Test writing best practices, CI/CD configuration
|
|
||||||
|
|
||||||
## Artifacts & Links
|
|
||||||
- Test results: `test-results/` directory
|
|
||||||
- Error context: `error-context.md` files with page snapshots
|
|
||||||
- Trace files: `trace.zip` files for failed tests
|
|
||||||
- HTML reports: Interactive test reports with screenshots
|
|
||||||
|
|
||||||
## Environment & Preconditions
|
|
||||||
- OS/Runtime: Linux/Windows/macOS with Node.js
|
|
||||||
- Versions: Playwright test framework, browser drivers
|
|
||||||
- Services: Local test server (localhost:8080), test data setup
|
|
||||||
- Auth mode: None required for test investigation
|
|
||||||
|
|
||||||
## Architecture / Process Overview
|
|
||||||
Playwright test investigation follows a systematic diagnostic workflow that leverages built-in debugging tools and error context analysis.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A[Test Failure] --> B[Check Error Context]
|
|
||||||
B --> C[Analyze Page Snapshot]
|
|
||||||
C --> D[Identify UI Conflicts]
|
|
||||||
D --> E[Check Trace Files]
|
|
||||||
E --> F[Verify Selector Uniqueness]
|
|
||||||
F --> G[Test Selector Fixes]
|
|
||||||
G --> H[Document Root Cause]
|
|
||||||
|
|
||||||
B --> I[Check Test Results Directory]
|
|
||||||
I --> J[Locate Failed Test Results]
|
|
||||||
J --> K[Extract Error Details]
|
|
||||||
|
|
||||||
D --> L[Multiple Alerts?]
|
|
||||||
L --> M[Button Text Conflicts?]
|
|
||||||
M --> N[Timing Issues?]
|
|
||||||
|
|
||||||
E --> O[Use Trace Viewer]
|
|
||||||
O --> P[Analyze Action Sequence]
|
|
||||||
P --> Q[Identify Failure Point]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Interfaces & Contracts
|
|
||||||
|
|
||||||
### Test Results Structure
|
|
||||||
| Component | Format | Content | Validation |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Error Context | Markdown | Page snapshot in YAML | Verify DOM state matches test expectations |
|
|
||||||
| Trace Files | ZIP archive | Detailed execution trace | Use `npx playwright show-trace` |
|
|
||||||
| HTML Reports | Interactive HTML | Screenshots, traces, logs | Check browser for full report |
|
|
||||||
| JSON Results | JSON | Machine-readable results | Parse for automated analysis |
|
|
||||||
|
|
||||||
### Investigation Commands
|
|
||||||
| Step | Command | Expected Output | Notes |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Locate failed tests | `find test-results -name "*test-name*"` | Test result directories | Use exact test name patterns |
|
|
||||||
| Check error context | `cat test-results/*/error-context.md` | Page snapshots | Look for UI state conflicts |
|
|
||||||
| View traces | `npx playwright show-trace trace.zip` | Interactive trace viewer | Analyze exact failure sequence |
|
|
||||||
|
|
||||||
## Repro: End-to-End Investigation Procedure
|
|
||||||
|
|
||||||
### 1. Locate Failed Test Results
|
|
||||||
```bash
|
|
||||||
# Find all results for a specific test
|
|
||||||
find test-results -name "*test-name*" -type d
|
|
||||||
|
|
||||||
# Check for error context files
|
|
||||||
find test-results -name "error-context.md" | head -5
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Analyze Error Context
|
|
||||||
```bash
|
|
||||||
# Read error context for specific test
|
|
||||||
cat test-results/test-name-test-description-browser/error-context.md
|
|
||||||
|
|
||||||
# Look for UI conflicts in page snapshot
|
|
||||||
grep -A 10 -B 5 "button.*Yes\|button.*No" test-results/*/error-context.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Check Trace Files
|
|
||||||
```bash
|
|
||||||
# List available trace files
|
|
||||||
find test-results -name "*.zip" | grep trace
|
|
||||||
|
|
||||||
# View trace in browser
|
|
||||||
npx playwright show-trace test-results/test-name/trace.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Investigate Selector Issues
|
|
||||||
```typescript
|
|
||||||
// Check for multiple elements with same text
|
|
||||||
await page.locator('button:has-text("Yes")').count(); // Should be 1
|
|
||||||
|
|
||||||
// Use more specific selectors
|
|
||||||
await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes")').click();
|
|
||||||
```
|
|
||||||
|
|
||||||
## What Works (Evidence)
|
|
||||||
- ✅ **Error context files** provide page snapshots showing exact DOM state at failure
|
|
||||||
- **Time**: 2025-08-21T14:22Z
|
|
||||||
- **Evidence**: `test-results/60-new-activity-New-offers-for-another-user-chromium/error-context.md` shows both alerts visible
|
|
||||||
- **Verify at**: Error context files in test results directory
|
|
||||||
|
|
||||||
- ✅ **Trace files** capture detailed execution sequence for failed tests
|
|
||||||
- **Time**: 2025-08-21T14:22Z
|
|
||||||
- **Evidence**: `trace.zip` files available for all failed tests
|
|
||||||
- **Verify at**: Use `npx playwright show-trace <filename>`
|
|
||||||
|
|
||||||
- ✅ **Page snapshots** reveal UI conflicts like multiple alerts with duplicate button text
|
|
||||||
- **Time**: 2025-08-21T14:22Z
|
|
||||||
- **Evidence**: YAML snapshots show registration + export alerts simultaneously
|
|
||||||
- **Verify at**: Error context markdown files
|
|
||||||
|
|
||||||
## What Doesn't (Evidence & Hypotheses)
|
|
||||||
- ❌ **Generic selectors** fail with multiple similar elements at `test-playwright/testUtils.ts:161`
|
|
||||||
- **Time**: 2025-08-21T14:22Z
|
|
||||||
- **Evidence**: `button:has-text("Yes")` matches both "Yes" and "Yes, Export Data"
|
|
||||||
- **Hypothesis**: Selector ambiguity due to multiple alerts with conflicting button text
|
|
||||||
- **Next probe**: Use more specific selectors or dismiss alerts sequentially
|
|
||||||
|
|
||||||
- ❌ **Timing-dependent tests** fail due to alert stacking at `src/views/ContactsView.vue:860,1283`
|
|
||||||
- **Time**: 2025-08-21T14:22Z
|
|
||||||
- **Evidence**: Both alerts use identical 1000ms delays, ensuring simultaneous display
|
|
||||||
- **Hypothesis**: Race condition between alert displays creates UI conflicts
|
|
||||||
- **Next probe**: Implement alert queuing or prevent overlapping alerts
|
|
||||||
|
|
||||||
## Risks, Limits, Assumptions
|
|
||||||
- **Trace file size**: Large trace files may impact storage and analysis time
|
|
||||||
- **Browser compatibility**: Trace viewer requires specific browser support
|
|
||||||
- **Test isolation**: Shared state between tests may affect investigation results
|
|
||||||
- **Timing sensitivity**: Tests may pass/fail based on system performance
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
| Owner | Task | Exit Criteria | Target Date (UTC) |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Development Team | Fix test selectors for multiple alerts | All tests pass consistently | 2025-08-22 |
|
|
||||||
| Development Team | Implement alert queuing system | No overlapping alerts with conflicting buttons | 2025-08-25 |
|
|
||||||
| Development Team | Add test IDs to alert buttons | Unique selectors for all UI elements | 2025-08-28 |
|
|
||||||
|
|
||||||
## References
|
|
||||||
- [Playwright Trace Viewer Documentation](https://playwright.dev/docs/trace-viewer)
|
|
||||||
- [Playwright Test Results](https://playwright.dev/docs/test-reporters)
|
|
||||||
- [Test Investigation Workflow](./research_diagnostic.mdc)
|
|
||||||
|
|
||||||
## Competence Hooks
|
|
||||||
- **Why this works**: Systematic investigation leverages Playwright's built-in debugging tools to identify root causes
|
|
||||||
- **Common pitfalls**: Generic selectors fail with multiple similar elements; timing issues create race conditions; alert stacking causes UI conflicts
|
|
||||||
- **Next skill unlock**: Implement unique test IDs and handle alert dismissal order in test flows
|
|
||||||
- **Teach-back**: "How would you investigate a Playwright test failure using error context, trace files, and page snapshots?"
|
|
||||||
|
|
||||||
## Collaboration Hooks
|
|
||||||
- **Reviewers**: QA team, test automation engineers
|
|
||||||
- **Sign-off checklist**: Error context analyzed, trace files reviewed, root cause identified, fix implemented and tested
|
|
||||||
|
|
||||||
## Assumptions & Limits
|
|
||||||
- Test results directory structure follows Playwright conventions
|
|
||||||
- Trace files are enabled in configuration (`trace: "retain-on-failure"`)
|
|
||||||
- Error context files contain valid YAML page snapshots
|
|
||||||
- Browser environment supports trace viewer functionality
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status**: Active investigation directive
|
|
||||||
**Priority**: High
|
|
||||||
**Maintainer**: Development team
|
|
||||||
**Next Review**: 2025-09-21
|
|
||||||
@@ -1,714 +0,0 @@
|
|||||||
```json
|
|
||||||
{
|
|
||||||
"coaching_level": "standard",
|
|
||||||
"socratic_max_questions": 2,
|
|
||||||
"verbosity": "normal",
|
|
||||||
"timebox_minutes": null,
|
|
||||||
"format_enforcement": "strict"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Unit Testing & Mocks — Universal Development Guide
|
|
||||||
|
|
||||||
**Author**: Matthew Raymer
|
|
||||||
**Date**: 2025-08-21T09:40Z
|
|
||||||
**Status**: 🎯 **ACTIVE** - Comprehensive testing standards
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This guide establishes **unified unit testing and mocking standards** for Vue
|
|
||||||
and React projects, ensuring consistent, maintainable test patterns using
|
|
||||||
Vitest, JSDOM, and component testing utilities. All tests follow F.I.R.S.T.
|
|
||||||
principles with comprehensive mock implementations.
|
|
||||||
|
|
||||||
## Scope and Goals
|
|
||||||
|
|
||||||
**Scope**: Applies to all unit tests, mock implementations, and testing
|
|
||||||
infrastructure in any project workspace.
|
|
||||||
|
|
||||||
**Goal**: One consistent testing approach with comprehensive mock coverage,
|
|
||||||
100% test coverage for simple components, and maintainable test patterns.
|
|
||||||
|
|
||||||
## Non‑Negotiables (DO THIS)
|
|
||||||
|
|
||||||
- **MUST** use Vitest + JSDOM for unit testing; **DO NOT** use Jest or other
|
|
||||||
frameworks
|
|
||||||
- **MUST** implement comprehensive mock levels (Simple, Standard, Complex) for
|
|
||||||
all components
|
|
||||||
- **MUST** achieve 100% line coverage for simple components (<100 lines)
|
|
||||||
- **MUST** follow F.I.R.S.T. principles: Fast, Independent, Repeatable,
|
|
||||||
Self-validating, Timely
|
|
||||||
- **MUST** use centralized test utilities from `src/test/utils/`
|
|
||||||
|
|
||||||
## Testing Infrastructure
|
|
||||||
|
|
||||||
### **Core Technologies**
|
|
||||||
|
|
||||||
- **Vitest**: Fast unit testing framework with Vue/React support
|
|
||||||
- **JSDOM**: Browser-like environment for Node.js testing
|
|
||||||
- **@vue/test-utils**: Vue component testing utilities
|
|
||||||
- **TypeScript**: Full type safety for tests and mocks
|
|
||||||
|
|
||||||
### **Configuration Files**
|
|
||||||
|
|
||||||
- `vitest.config.ts` - Vitest configuration with JSDOM environment
|
|
||||||
- `src/test/setup.ts` - Global test configuration and mocks
|
|
||||||
- `src/test/utils/` - Centralized testing utilities
|
|
||||||
|
|
||||||
### **Global Mocks**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Required browser API mocks
|
|
||||||
ResizeObserver, IntersectionObserver, localStorage, sessionStorage,
|
|
||||||
matchMedia, console methods (reduced noise)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mock Implementation Standards
|
|
||||||
|
|
||||||
### **Mock Architecture Levels**
|
|
||||||
|
|
||||||
#### **1. Simple Mock (Basic Testing)**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Minimal interface compliance
|
|
||||||
class ComponentSimpleMock {
|
|
||||||
// Essential props and methods only
|
|
||||||
// Basic computed properties
|
|
||||||
// No complex behavior
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **2. Standard Mock (Integration Testing)**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Full interface compliance
|
|
||||||
class ComponentStandardMock {
|
|
||||||
// All props, methods, computed properties
|
|
||||||
// Realistic behavior simulation
|
|
||||||
// Helper methods for test scenarios
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **3. Complex Mock (Advanced Testing)**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Enhanced testing capabilities
|
|
||||||
class ComponentComplexMock extends ComponentStandardMock {
|
|
||||||
// Mock event listeners
|
|
||||||
// Performance testing hooks
|
|
||||||
// Error scenario simulation
|
|
||||||
// Accessibility testing support
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Mock Component Structure**
|
|
||||||
|
|
||||||
Each mock component provides:
|
|
||||||
|
|
||||||
- Same interface as original component
|
|
||||||
- Simplified behavior for testing
|
|
||||||
- Helper methods for test scenarios
|
|
||||||
- Computed properties for state validation
|
|
||||||
|
|
||||||
### **Enhanced Mock Architecture Validation** ✅ **NEW**
|
|
||||||
|
|
||||||
The three-tier mock architecture (Simple/Standard/Complex) has been successfully
|
|
||||||
validated through real-world implementation:
|
|
||||||
|
|
||||||
#### **Tier 1: Simple Mock**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class ComponentSimpleMock {
|
|
||||||
// Basic interface compliance
|
|
||||||
// Minimal implementation for simple tests
|
|
||||||
// Fast execution for high-volume testing
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Tier 2: Standard Mock**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class ComponentStandardMock {
|
|
||||||
// Full interface implementation
|
|
||||||
// Realistic behavior simulation
|
|
||||||
// Helper methods for common scenarios
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Tier 3: Complex Mock**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class ComponentComplexMock {
|
|
||||||
// Enhanced testing capabilities
|
|
||||||
// Validation and error simulation
|
|
||||||
// Advanced state management
|
|
||||||
// Performance testing support
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Factory Function Pattern**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Specialized factory functions for common use cases
|
|
||||||
export const createComponentMock = () =>
|
|
||||||
new ComponentStandardMock({ type: 'default' })
|
|
||||||
|
|
||||||
export const createSpecializedMock = () =>
|
|
||||||
new ComponentComplexMock({
|
|
||||||
options: { filter: 'active', sort: 'name' }
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Mock Usage Examples**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export default class ComponentMock {
|
|
||||||
// Props simulation
|
|
||||||
props: ComponentProps
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
get computedProp(): boolean {
|
|
||||||
return this.props.condition
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock methods
|
|
||||||
mockMethod(): void {
|
|
||||||
// Simulate behavior
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
getCssClasses(): string[] {
|
|
||||||
return ['base-class', 'conditional-class']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Patterns
|
|
||||||
|
|
||||||
### **Component Testing Template**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { createComponentWrapper } from '@/test/utils/componentTestUtils'
|
|
||||||
|
|
||||||
describe('ComponentName', () => {
|
|
||||||
let wrapper: VueWrapper<any>
|
|
||||||
|
|
||||||
const mountComponent = (props = {}) => {
|
|
||||||
return mount(ComponentName, {
|
|
||||||
props: { ...defaultProps, ...props }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountComponent()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
wrapper?.unmount()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Component Rendering', () => {
|
|
||||||
it('should render correctly', () => {
|
|
||||||
expect(wrapper.exists()).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Mock Integration Testing**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import ComponentMock from '@/test/__mocks__/Component.mock'
|
|
||||||
|
|
||||||
it('should work with mock component', () => {
|
|
||||||
const mock = new ComponentMock()
|
|
||||||
expect(mock.shouldShow).toBe(true)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Event Testing**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it('should emit event when triggered', async () => {
|
|
||||||
await wrapper.find('button').trigger('click')
|
|
||||||
expect(wrapper.emitted('event-name')).toBeTruthy()
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Prop Validation**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it('should accept all required props', () => {
|
|
||||||
wrapper = mountComponent()
|
|
||||||
expect(wrapper.vm.propName).toBeDefined()
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Categories
|
|
||||||
|
|
||||||
### **Required Coverage Areas**
|
|
||||||
|
|
||||||
1. **Component Rendering** - Existence, structure, conditional rendering
|
|
||||||
2. **Component Styling** - CSS classes, responsive design, framework
|
|
||||||
integration
|
|
||||||
3. **Component Props** - Required/optional prop handling, type validation
|
|
||||||
4. **User Interactions** - Click events, form inputs, keyboard navigation
|
|
||||||
5. **Component Methods** - Method existence, functionality, return values
|
|
||||||
6. **Edge Cases** - Empty/null props, rapid interactions, state changes
|
|
||||||
7. **Error Handling** - Invalid props, malformed data, graceful degradation
|
|
||||||
8. **Accessibility** - Semantic HTML, ARIA attributes, keyboard navigation
|
|
||||||
9. **Performance** - Render time, memory leaks, rapid re-renders
|
|
||||||
10. **Integration** - Parent-child interaction, dependency injection
|
|
||||||
|
|
||||||
### **Error Handling Testing**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const invalidPropCombinations = [
|
|
||||||
null, undefined, 'invalid', 0, -1, {}, [],
|
|
||||||
() => {}, NaN, Infinity
|
|
||||||
]
|
|
||||||
|
|
||||||
invalidPropCombinations.forEach(invalidProp => {
|
|
||||||
it(`should handle invalid prop: ${invalidProp}`, () => {
|
|
||||||
wrapper = mountComponent({ prop: invalidProp })
|
|
||||||
expect(wrapper.exists()).toBe(true)
|
|
||||||
// Verify graceful handling
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Centralized Test Utilities
|
|
||||||
|
|
||||||
### **Component Testing Utilities**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
createComponentWrapper,
|
|
||||||
createTestDataFactory,
|
|
||||||
testLifecycleEvents,
|
|
||||||
testComputedProperties,
|
|
||||||
testWatchers,
|
|
||||||
testPerformance,
|
|
||||||
testAccessibility,
|
|
||||||
testErrorHandling
|
|
||||||
} from '@/test/utils/componentTestUtils'
|
|
||||||
|
|
||||||
// Component wrapper factory
|
|
||||||
const wrapperFactory = createComponentWrapper(
|
|
||||||
Component,
|
|
||||||
defaultProps,
|
|
||||||
globalOptions
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test data factory
|
|
||||||
const createTestProps = createTestDataFactory({
|
|
||||||
prop1: 'default',
|
|
||||||
prop2: true
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Test Data Factories**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
createMockContact,
|
|
||||||
createMockProject,
|
|
||||||
createMockUser
|
|
||||||
} from '@/test/factories/contactFactory'
|
|
||||||
|
|
||||||
const testContact = createMockContact({
|
|
||||||
id: 'test-1',
|
|
||||||
name: 'Test User'
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Coverage Standards
|
|
||||||
|
|
||||||
### **Coverage Standards by Component Complexity**
|
|
||||||
|
|
||||||
| Component Complexity | Line Coverage | Branch Coverage | Function Coverage |
|
|
||||||
|---------------------|---------------|-----------------|-------------------|
|
|
||||||
| **Simple (<100 lines)** | 100% | 100% | 100% |
|
|
||||||
| **Medium (100-300 lines)** | 95% | 90% | 100% |
|
|
||||||
| **Complex (300+ lines)** | 90% | 85% | 100% |
|
|
||||||
|
|
||||||
### **Current Coverage Status**
|
|
||||||
|
|
||||||
- **Simple Components**: Ready for implementation
|
|
||||||
- **Medium Components**: Ready for expansion
|
|
||||||
- **Complex Components**: Ready for expansion
|
|
||||||
- **Overall Coverage**: Varies by project implementation
|
|
||||||
|
|
||||||
### **Test Infrastructure Requirements**
|
|
||||||
|
|
||||||
- **Test Framework**: Vitest + JSDOM recommended
|
|
||||||
- **Component Testing**: Vue Test Utils integration
|
|
||||||
- **Mock Architecture**: Three-tier system (Simple/Standard/Complex)
|
|
||||||
- **Test Categories**: 10 comprehensive categories
|
|
||||||
- **Coverage Goals**: 100% for simple components, 90%+ for complex
|
|
||||||
|
|
||||||
## Testing Philosophy
|
|
||||||
|
|
||||||
### **Defensive Programming Validation**
|
|
||||||
|
|
||||||
- **Real-world edge case protection** against invalid API responses
|
|
||||||
- **System stability assurance** preventing cascading failures
|
|
||||||
- **Production readiness** ensuring graceful error handling
|
|
||||||
|
|
||||||
### **Comprehensive Error Scenarios**
|
|
||||||
|
|
||||||
- **Invalid input testing** with 10+ different invalid prop combinations
|
|
||||||
- **Malformed data testing** with various corrupted data structures
|
|
||||||
- **Extreme value testing** with boundary conditions and edge cases
|
|
||||||
- **Concurrent error testing** with rapid state changes
|
|
||||||
|
|
||||||
### **Benefits Beyond Coverage**
|
|
||||||
|
|
||||||
1. **Defensive Programming Validation** - Components handle unexpected data
|
|
||||||
gracefully
|
|
||||||
2. **Real-World Resilience** - Tested against actual failure scenarios
|
|
||||||
3. **Developer Confidence** - Safe to refactor and extend components
|
|
||||||
4. **Production Stability** - Reduced support tickets and user complaints
|
|
||||||
|
|
||||||
## Advanced Testing Patterns
|
|
||||||
|
|
||||||
### **Performance Testing** ✅ **NEW**
|
|
||||||
|
|
||||||
- Render time benchmarks
|
|
||||||
- Memory leak detection
|
|
||||||
- Rapid re-render efficiency
|
|
||||||
- Component cleanup validation
|
|
||||||
|
|
||||||
#### **Advanced Performance Testing Patterns**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Memory leak detection
|
|
||||||
it('should not cause memory leaks during prop changes', async () => {
|
|
||||||
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0
|
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
await wrapper.setProps({
|
|
||||||
queryParams: { iteration: i.toString() }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalMemory = (performance as any).memory?.usedJSHeapSize || 0
|
|
||||||
const memoryIncrease = finalMemory - initialMemory
|
|
||||||
|
|
||||||
// Memory increase should be reasonable (less than 10MB)
|
|
||||||
expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Rapid re-render efficiency
|
|
||||||
it('should handle rapid re-renders efficiently', async () => {
|
|
||||||
const start = performance.now()
|
|
||||||
|
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
await wrapper.setProps({
|
|
||||||
entityType: i % 2 === 0 ? 'type1' : 'type2',
|
|
||||||
queryParams: { index: i.toString() }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const end = performance.now()
|
|
||||||
expect(end - start).toBeLessThan(500) // 500ms threshold for 50 updates
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Snapshot Testing** ✅ **NEW**
|
|
||||||
|
|
||||||
- DOM structure validation
|
|
||||||
- CSS class regression detection
|
|
||||||
- Accessibility attribute consistency
|
|
||||||
- Visual structure verification
|
|
||||||
|
|
||||||
#### **Snapshot Testing Implementation**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
describe('Snapshot Testing', () => {
|
|
||||||
it('should maintain consistent DOM structure', () => {
|
|
||||||
expect(wrapper.html()).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should maintain consistent structure with different props', () => {
|
|
||||||
wrapper = mountComponent({ type: 'alternative' })
|
|
||||||
expect(wrapper.html()).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should maintain consistent structure with query params', () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
queryParams: { filter: 'active', sort: 'name' }
|
|
||||||
})
|
|
||||||
expect(wrapper.html()).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Mock Integration Testing** ✅ **NEW**
|
|
||||||
|
|
||||||
- Mock component validation
|
|
||||||
- Factory function testing
|
|
||||||
- Mock behavior verification
|
|
||||||
- Integration with testing utilities
|
|
||||||
|
|
||||||
#### **Mock Integration Testing Patterns**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
describe('Mock Integration Testing', () => {
|
|
||||||
it('should work with simple mock', () => {
|
|
||||||
const mock = new ComponentSimpleMock()
|
|
||||||
expect(mock.navigationRoute).toEqual({
|
|
||||||
name: 'default',
|
|
||||||
query: {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should work with standard mock', () => {
|
|
||||||
const mock = new ComponentStandardMock({
|
|
||||||
type: 'special',
|
|
||||||
name: 'test'
|
|
||||||
})
|
|
||||||
expect(mock.getType()).toBe('special')
|
|
||||||
expect(mock.getName()).toBe('test')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should work with complex mock', () => {
|
|
||||||
const mock = new ComponentComplexMock({
|
|
||||||
type: 'advanced',
|
|
||||||
options: { filter: 'active' }
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(mock.isValidState()).toBe(true)
|
|
||||||
expect(mock.getValidationErrors()).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should work with factory functions', () => {
|
|
||||||
const defaultMock = createComponentMock()
|
|
||||||
const specializedMock = createSpecializedMock()
|
|
||||||
|
|
||||||
expect(defaultMock.getType()).toBe('default')
|
|
||||||
expect(specializedMock.getOptions()).toHaveProperty('filter')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Implementation Tracking
|
|
||||||
|
|
||||||
### **Setting Up Project-Specific Tracking**
|
|
||||||
|
|
||||||
Each project should maintain its own tracking file to monitor testing progress
|
|
||||||
and coverage metrics. This keeps the universal MDC clean while providing a
|
|
||||||
template for project implementation.
|
|
||||||
|
|
||||||
#### **Recommended Project Tracking Structure**
|
|
||||||
|
|
||||||
```tree
|
|
||||||
src/test/
|
|
||||||
├── README.md # Testing documentation
|
|
||||||
├── PROJECT_COVERAGE_TRACKING.md # Project-specific progress tracking
|
|
||||||
├── __mocks__/ # Mock implementations
|
|
||||||
├── utils/ # Test utilities
|
|
||||||
└── [test files]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Project Tracking File Template**
|
|
||||||
|
|
||||||
Create a `PROJECT_COVERAGE_TRACKING.md` file with:
|
|
||||||
|
|
||||||
- **Current Coverage Status**: Component-by-component breakdown
|
|
||||||
- **Implementation Progress**: Phase completion status
|
|
||||||
- **Test Infrastructure Status**: Framework setup and metrics
|
|
||||||
- **Next Steps**: Immediate priorities and long-term goals
|
|
||||||
- **Lessons Learned**: Project-specific insights and best practices
|
|
||||||
|
|
||||||
#### **Example Project Tracking Sections**
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# [Project Name] Testing Coverage Tracking
|
|
||||||
|
|
||||||
## Current Coverage Status
|
|
||||||
- Simple Components: X/Y at 100% coverage
|
|
||||||
- Medium Components: X/Y ready for expansion
|
|
||||||
- Complex Components: X/Y planned
|
|
||||||
|
|
||||||
## Implementation Progress
|
|
||||||
- Phase 1: Simple Components ✅ COMPLETE
|
|
||||||
- Phase 2: Medium Components 🔄 IN PROGRESS
|
|
||||||
- Phase 3: Complex Components 🔄 PLANNED
|
|
||||||
|
|
||||||
## Test Infrastructure Status
|
|
||||||
- Total Tests: X tests passing
|
|
||||||
- Test Files: X files
|
|
||||||
- Mock Files: X implementations
|
|
||||||
- Overall Coverage: X% (focused on simple components)
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Integration with Universal MDC**
|
|
||||||
|
|
||||||
- **MDC provides**: Testing patterns, mock architecture, best practices
|
|
||||||
- **Project tracking provides**: Implementation status, coverage metrics,
|
|
||||||
progress
|
|
||||||
- **Separation ensures**: MDC remains reusable, project data stays local
|
|
||||||
- **Template approach**: Other projects can copy and adapt the structure
|
|
||||||
|
|
||||||
### **Benefits of This Approach**
|
|
||||||
|
|
||||||
1. **Universal Reusability**: MDC works for any project
|
|
||||||
2. **Project Visibility**: Clear tracking of implementation progress
|
|
||||||
3. **Template Reuse**: Easy to set up tracking in new projects
|
|
||||||
4. **Clean Separation**: No project data polluting universal guidance
|
|
||||||
5. **Scalability**: Multiple projects can use the same MDC
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### **Test Organization**
|
|
||||||
|
|
||||||
1. **Group related tests** using `describe` blocks
|
|
||||||
2. **Use descriptive test names** that explain the scenario
|
|
||||||
3. **Keep tests focused** on one specific behavior
|
|
||||||
4. **Use helper functions** for common setup
|
|
||||||
|
|
||||||
### **Mock Design**
|
|
||||||
|
|
||||||
1. **Maintain interface compatibility** with original components
|
|
||||||
2. **Provide helper methods** for common test scenarios
|
|
||||||
3. **Include computed properties** for state validation
|
|
||||||
4. **Document mock behavior** clearly
|
|
||||||
|
|
||||||
### **Coverage Goals**
|
|
||||||
|
|
||||||
1. **100% line coverage** for simple components
|
|
||||||
2. **100% branch coverage** for conditional logic
|
|
||||||
3. **100% function coverage** for all methods
|
|
||||||
4. **Edge case coverage** for error scenarios
|
|
||||||
|
|
||||||
### **Lessons Learned from Implementation** ✅ **NEW**
|
|
||||||
|
|
||||||
#### **1. Performance Testing Best Practices**
|
|
||||||
|
|
||||||
- **Memory leak detection**: Use `performance.memory.usedJSHeapSize` for
|
|
||||||
memory profiling
|
|
||||||
- **Render time benchmarking**: Set realistic thresholds (100ms for single
|
|
||||||
render, 500ms for 50 updates)
|
|
||||||
- **Rapid re-render testing**: Test with 50+ prop changes to ensure
|
|
||||||
stability
|
|
||||||
|
|
||||||
#### **2. Snapshot Testing Implementation**
|
|
||||||
|
|
||||||
- **DOM structure validation**: Use `toMatchSnapshot()` for consistent
|
|
||||||
structure verification
|
|
||||||
- **Prop variation testing**: Test snapshots with different prop combinations
|
|
||||||
- **Regression prevention**: Snapshots catch unexpected DOM changes
|
|
||||||
|
|
||||||
#### **3. Mock Integration Validation**
|
|
||||||
|
|
||||||
- **Mock self-testing**: Test that mocks work correctly with testing
|
|
||||||
utilities
|
|
||||||
- **Factory function testing**: Validate specialized factory functions
|
|
||||||
- **Mock behavior verification**: Ensure mocks simulate real component
|
|
||||||
behavior
|
|
||||||
|
|
||||||
#### **4. Edge Case Coverage**
|
|
||||||
|
|
||||||
- **Null/undefined handling**: Test with `null as any` and `undefined`
|
|
||||||
props
|
|
||||||
- **Extreme values**: Test with very long strings and large numbers
|
|
||||||
- **Rapid changes**: Test with rapid prop changes to ensure stability
|
|
||||||
|
|
||||||
#### **5. Accessibility Testing**
|
|
||||||
|
|
||||||
- **Semantic structure**: Verify proper HTML elements and hierarchy
|
|
||||||
- **Component attributes**: Check component-specific attributes
|
|
||||||
- **Text content**: Validate text content and trimming
|
|
||||||
|
|
||||||
## Future Improvements
|
|
||||||
|
|
||||||
### **Implemented Enhancements**
|
|
||||||
|
|
||||||
1. ✅ **Error handling** - Component error states and exception handling
|
|
||||||
2. ✅ **Performance testing** - Render time benchmarks and memory leak
|
|
||||||
detection
|
|
||||||
3. ✅ **Integration testing** - Parent-child component interaction and
|
|
||||||
dependency injection
|
|
||||||
4. ✅ **Snapshot testing** - DOM structure validation and CSS class
|
|
||||||
regression detection
|
|
||||||
5. ✅ **Accessibility compliance** - ARIA attributes and semantic structure
|
|
||||||
validation
|
|
||||||
|
|
||||||
### **Future Enhancements**
|
|
||||||
|
|
||||||
1. **Visual regression testing** - Automated UI consistency checks
|
|
||||||
2. **Cross-browser compatibility** testing
|
|
||||||
3. **Service layer integration** testing
|
|
||||||
4. **End-to-end component** testing
|
|
||||||
5. **Advanced performance** profiling
|
|
||||||
|
|
||||||
### **Coverage Expansion**
|
|
||||||
|
|
||||||
1. **Medium complexity components** (100-300 lines)
|
|
||||||
2. **Complex components** (300+ lines)
|
|
||||||
3. **Service layer testing**
|
|
||||||
4. **Utility function testing**
|
|
||||||
5. **API integration testing**
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### **Common Issues**
|
|
||||||
|
|
||||||
1. **Import errors**: Check path aliases in `vitest.config.ts`
|
|
||||||
2. **Mock not found**: Verify mock file exists and exports correctly
|
|
||||||
3. **Test failures**: Check for timing issues with async operations
|
|
||||||
4. **Coverage gaps**: Add tests for uncovered code paths
|
|
||||||
|
|
||||||
### **Debug Tips**
|
|
||||||
|
|
||||||
1. **Use `console.log`** in tests for debugging
|
|
||||||
2. **Check test output** for detailed error messages
|
|
||||||
3. **Verify component props** are being passed correctly
|
|
||||||
4. **Test one assertion at a time** to isolate issues
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status**: Active testing standards
|
|
||||||
**Priority**: High
|
|
||||||
**Estimated Effort**: Ongoing reference
|
|
||||||
**Dependencies**: Vitest, JSDOM, Vue Test Utils
|
|
||||||
**Stakeholders**: Development team, QA team
|
|
||||||
|
|
||||||
## Competence Hooks
|
|
||||||
|
|
||||||
- *Why this works*: Three-tier mock architecture provides flexibility,
|
|
||||||
comprehensive test categories ensure thorough coverage, performance testing
|
|
||||||
catches real-world issues early
|
|
||||||
- *Common pitfalls*: Not testing mocks themselves, missing edge case
|
|
||||||
coverage, ignoring performance implications
|
|
||||||
- *Next skill unlock*: Implement medium complexity component testing with
|
|
||||||
established patterns
|
|
||||||
- *Teach-back*: Explain how the three-tier mock architecture supports
|
|
||||||
different testing needs
|
|
||||||
|
|
||||||
## Collaboration Hooks
|
|
||||||
|
|
||||||
- **Reviewers**: Testing team, component developers, architecture team
|
|
||||||
- **Sign-off checklist**: All simple components at 100% coverage, mock
|
|
||||||
utilities documented, test patterns established, coverage expansion plan
|
|
||||||
approved
|
|
||||||
|
|
||||||
## Assumptions & Limits
|
|
||||||
|
|
||||||
- Assumes Vue/React component architecture
|
|
||||||
- Requires Vitest + JSDOM testing environment
|
|
||||||
- Mock complexity scales with component complexity
|
|
||||||
- Performance testing requires browser-like environment
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Vitest Documentation](https://vitest.dev/)
|
|
||||||
- [Vue Test Utils](https://test-utils.vuejs.org/)
|
|
||||||
- [JSDOM](https://github.com/jsdom/jsdom)
|
|
||||||
- [Testing Best Practices](https://testing-library.com/docs/guiding-principles)
|
|
||||||
|
|
||||||
- **Sign-off checklist**: All simple components at 100% coverage, mock
|
|
||||||
utilities documented, test patterns established, coverage expansion plan
|
|
||||||
approved
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -140,6 +140,4 @@ electron/out/
|
|||||||
# Gradle cache files
|
# Gradle cache files
|
||||||
android/.gradle/file-system.probe
|
android/.gradle/file-system.probe
|
||||||
android/.gradle/caches/
|
android/.gradle/caches/
|
||||||
|
coverage
|
||||||
coverage/
|
|
||||||
.husky-enabled
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Husky Git Hooks - Optional Activation
|
|
||||||
|
|
||||||
## How to Enable Husky Locally
|
|
||||||
|
|
||||||
### Option 1: Environment Variable (Session Only)
|
|
||||||
```bash
|
|
||||||
export HUSKY_ENABLED=1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 2: Local File (Persistent)
|
|
||||||
```bash
|
|
||||||
touch .husky-enabled
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option 3: Global Configuration
|
|
||||||
```bash
|
|
||||||
git config --global husky.enabled true
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Hooks
|
|
||||||
|
|
||||||
- **pre-commit**: Runs `npm run lint-fix` before commits
|
|
||||||
- **commit-msg**: Validates commit message format
|
|
||||||
|
|
||||||
## Disable Hooks
|
|
||||||
|
|
||||||
```bash
|
|
||||||
unset HUSKY_ENABLED
|
|
||||||
rm .husky-enabled
|
|
||||||
```
|
|
||||||
|
|
||||||
## Why This Approach?
|
|
||||||
|
|
||||||
- Hooks are committed to git for consistency
|
|
||||||
- Hooks don't run unless explicitly enabled
|
|
||||||
- Each developer can choose to use them
|
|
||||||
- No automatic activation on other systems
|
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
#
|
#
|
||||||
# Husky Helper Script - Conditional Activation
|
# Husky Helper Script
|
||||||
# This file is sourced by all Husky hooks
|
# This file is sourced by all Husky hooks
|
||||||
#
|
#
|
||||||
if [ -z "$husky_skip_init" ]; then
|
if [ -z "$husky_skip_init" ]; then
|
||||||
# Check if Husky is enabled for this user
|
|
||||||
if [ "$HUSKY_ENABLED" != "1" ] && [ ! -f .husky-enabled ]; then
|
|
||||||
echo "Husky is not enabled. To enable:"
|
|
||||||
echo " export HUSKY_ENABLED=1"
|
|
||||||
echo " or create .husky-enabled file"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
debug () {
|
debug () {
|
||||||
if [ "$HUSKY_DEBUG" = "1" ]; then
|
if [ "$HUSKY_DEBUG" = "1" ]; then
|
||||||
echo "husky (debug) - $1"
|
echo "husky (debug) - $1"
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Husky Commit Message Hook
|
||||||
|
# Validates commit message format using commitlint
|
||||||
|
#
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
# Only run if Husky is enabled
|
# Run commitlint but don't fail the commit (|| true)
|
||||||
if [ "$HUSKY_ENABLED" = "1" ] || [ -f .husky-enabled ]; then
|
# This provides helpful feedback without blocking commits
|
||||||
echo "Running commit-msg hooks..."
|
npx commitlint --edit "$1" || true
|
||||||
npx commitlint --edit "$1"
|
|
||||||
else
|
|
||||||
echo "Husky commit-msg hook skipped (not enabled)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Husky Pre-commit Hook
|
||||||
|
# Runs Build Architecture Guard to check staged files
|
||||||
|
#
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
# Only run if Husky is enabled
|
echo "🔍 Running Build Architecture Guard (pre-commit)..."
|
||||||
if [ "$HUSKY_ENABLED" = "1" ] || [ -f .husky-enabled ]; then
|
bash ./scripts/build-arch-guard.sh --staged || {
|
||||||
echo "Running pre-commit hooks..."
|
echo
|
||||||
npm run lint-fix
|
echo "💡 To bypass this check for emergency commits, use:"
|
||||||
else
|
echo " git commit --no-verify"
|
||||||
echo "Husky pre-commit hook skipped (not enabled)"
|
echo
|
||||||
exit 0
|
exit 1
|
||||||
fi
|
}
|
||||||
|
|||||||
184
TODO.md
184
TODO.md
@@ -1,184 +0,0 @@
|
|||||||
# Test Improvements TODO
|
|
||||||
|
|
||||||
## ImageViewer Mock Units - Completed ✅
|
|
||||||
- [x] Create comprehensive mock units for ImageViewer component
|
|
||||||
- [x] Implement 4 mock levels (Simple, Standard, Complex, Integration)
|
|
||||||
- [x] Fix template structure issues (Teleport/Transition complexity)
|
|
||||||
- [x] Resolve event simulation problems (SupportedEventInterface errors)
|
|
||||||
- [x] Fix platform detection logic (mobile vs desktop)
|
|
||||||
- [x] Implement analytics tracking in integration mock
|
|
||||||
- [x] Achieve 38/39 tests passing (97% success rate)
|
|
||||||
|
|
||||||
## Immediate Test Improvements Needed 🔧
|
|
||||||
|
|
||||||
### 1. Fix Remaining ImageViewer Test
|
|
||||||
- [ ] **Fix mobile share button test** - Vue reactivity issue with computed properties
|
|
||||||
- [ ] Investigate Vue 3 reactivity system for computed properties
|
|
||||||
- [ ] Try different approaches: `nextTick()`, `flushPromises()`, or reactive refs
|
|
||||||
- [ ] Consider using `shallowRef()` for userAgent to force reactivity
|
|
||||||
|
|
||||||
### 2. Event Simulation Improvements
|
|
||||||
- [ ] **Create global event simulation utilities**
|
|
||||||
- [ ] Build `triggerEvent()` helper that works with Vue Test Utils
|
|
||||||
- [ ] Handle `SupportedEventInterface` errors consistently
|
|
||||||
- [ ] Create fallback methods for problematic event types
|
|
||||||
- [ ] **Improve test environment setup**
|
|
||||||
- [ ] Configure proper DOM environment for event simulation
|
|
||||||
- [ ] Mock browser APIs more comprehensively
|
|
||||||
- [ ] Add global test utilities for common patterns
|
|
||||||
|
|
||||||
### 3. Mock Architecture Enhancements
|
|
||||||
- [ ] **Create reusable mock patterns**
|
|
||||||
- [ ] Extract common mock utilities (`createMockUserAgent`, etc.)
|
|
||||||
- [ ] Build mock factory patterns for other components
|
|
||||||
- [ ] Create mock validation helpers
|
|
||||||
- [ ] **Improve mock documentation**
|
|
||||||
- [ ] Add JSDoc comments to all mock functions
|
|
||||||
- [ ] Create usage examples for each mock level
|
|
||||||
- [ ] Document mock limitations and workarounds
|
|
||||||
|
|
||||||
## Component-Specific Test Improvements 🧪
|
|
||||||
|
|
||||||
### 4. Expand Mock Units to Other Components
|
|
||||||
- [ ] **QR Scanner Component**
|
|
||||||
- [ ] Create mock for `WebInlineQRScanner`
|
|
||||||
- [ ] Mock camera permissions and device detection
|
|
||||||
- [ ] Test platform-specific behavior (web vs mobile)
|
|
||||||
- [ ] **Platform Service Components**
|
|
||||||
- [ ] Mock `CapacitorPlatformService`
|
|
||||||
- [ ] Mock `WebPlatformService`
|
|
||||||
- [ ] Mock `ElectronPlatformService`
|
|
||||||
- [ ] **Database Components**
|
|
||||||
- [ ] Mock `AbsurdSqlDatabaseService`
|
|
||||||
- [ ] Test migration scenarios
|
|
||||||
- [ ] Mock IndexedDB operations
|
|
||||||
|
|
||||||
### 5. Integration Test Improvements
|
|
||||||
- [ ] **Cross-component communication**
|
|
||||||
- [ ] Test ImageViewer + QR Scanner integration
|
|
||||||
- [ ] Test platform service + component interactions
|
|
||||||
- [ ] Mock complex user workflows
|
|
||||||
- [ ] **End-to-end scenarios**
|
|
||||||
- [ ] Complete user journeys (scan → view → share)
|
|
||||||
- [ ] Error recovery flows
|
|
||||||
- [ ] Performance testing scenarios
|
|
||||||
|
|
||||||
## Test Infrastructure Improvements 🏗️
|
|
||||||
|
|
||||||
### 6. Test Environment Setup
|
|
||||||
- [ ] **Improve Vitest configuration**
|
|
||||||
- [ ] Add proper DOM environment setup
|
|
||||||
- [ ] Configure global mocks for browser APIs
|
|
||||||
- [ ] Add test utilities for common patterns
|
|
||||||
- [ ] **Create test helpers**
|
|
||||||
- [ ] `createComponentWrapper()` utility
|
|
||||||
- [ ] `mockPlatformService()` helper
|
|
||||||
- [ ] `simulateUserInteraction()` utilities
|
|
||||||
|
|
||||||
### 7. Performance Testing
|
|
||||||
- [ ] **Add performance benchmarks**
|
|
||||||
- [ ] Component render time testing
|
|
||||||
- [ ] Memory usage monitoring
|
|
||||||
- [ ] Image loading performance tests
|
|
||||||
- [ ] **Load testing scenarios**
|
|
||||||
- [ ] Multiple ImageViewer instances
|
|
||||||
- [ ] Large image handling
|
|
||||||
- [ ] Concurrent operations
|
|
||||||
|
|
||||||
## Quality Assurance Improvements 📊
|
|
||||||
|
|
||||||
### 8. Test Coverage Enhancement
|
|
||||||
- [ ] **Add missing test scenarios**
|
|
||||||
- [ ] Edge cases for image formats
|
|
||||||
- [ ] Network error handling
|
|
||||||
- [ ] Accessibility compliance tests
|
|
||||||
- [ ] **Mutation testing**
|
|
||||||
- [ ] Verify test quality with mutation testing
|
|
||||||
- [ ] Ensure tests catch actual bugs
|
|
||||||
- [ ] Improve test reliability
|
|
||||||
|
|
||||||
### 9. Test Documentation
|
|
||||||
- [ ] **Create test guidelines**
|
|
||||||
- [ ] Best practices for Vue component testing
|
|
||||||
- [ ] Mock unit design patterns
|
|
||||||
- [ ] Troubleshooting common test issues
|
|
||||||
- [ ] **Add test examples**
|
|
||||||
- [ ] Example test files for each component type
|
|
||||||
- [ ] Integration test examples
|
|
||||||
- [ ] Performance test examples
|
|
||||||
|
|
||||||
## Advanced Testing Features 🚀
|
|
||||||
|
|
||||||
### 10. Visual Regression Testing
|
|
||||||
- [ ] **Add visual testing**
|
|
||||||
- [ ] Screenshot comparison for ImageViewer
|
|
||||||
- [ ] Visual diff testing for UI changes
|
|
||||||
- [ ] Cross-platform visual consistency
|
|
||||||
- [ ] **Accessibility testing**
|
|
||||||
- [ ] Automated accessibility checks
|
|
||||||
- [ ] Screen reader compatibility tests
|
|
||||||
- [ ] Keyboard navigation testing
|
|
||||||
|
|
||||||
### 11. Contract Testing
|
|
||||||
- [ ] **API contract testing**
|
|
||||||
- [ ] Test component prop contracts
|
|
||||||
- [ ] Event emission contracts
|
|
||||||
- [ ] Service interface contracts
|
|
||||||
- [ ] **Mock contract validation**
|
|
||||||
- [ ] Ensure mocks match real component behavior
|
|
||||||
- [ ] Validate mock completeness
|
|
||||||
- [ ] Test mock accuracy
|
|
||||||
|
|
||||||
## Priority Levels 📋
|
|
||||||
|
|
||||||
### High Priority (Next Sprint)
|
|
||||||
1. Fix mobile share button test
|
|
||||||
2. Create global event simulation utilities
|
|
||||||
3. Expand mock units to QR Scanner component
|
|
||||||
4. Improve test environment setup
|
|
||||||
|
|
||||||
### Medium Priority (Next Month)
|
|
||||||
1. Create reusable mock patterns
|
|
||||||
2. Add performance testing
|
|
||||||
3. Improve test documentation
|
|
||||||
4. Add visual regression testing
|
|
||||||
|
|
||||||
### Low Priority (Future)
|
|
||||||
1. Advanced integration testing
|
|
||||||
2. Contract testing
|
|
||||||
3. Mutation testing
|
|
||||||
4. Cross-platform visual testing
|
|
||||||
|
|
||||||
## Success Metrics 📈
|
|
||||||
|
|
||||||
### Current Status
|
|
||||||
- ✅ **97% test pass rate** (38/39 tests)
|
|
||||||
- ✅ **4 mock levels** implemented
|
|
||||||
- ✅ **Comprehensive coverage** of ImageViewer functionality
|
|
||||||
- ✅ **Behavior-focused testing** approach working
|
|
||||||
|
|
||||||
### Target Metrics
|
|
||||||
- [ ] **100% test pass rate** (fix remaining test)
|
|
||||||
- [ ] **10+ components** with mock units
|
|
||||||
- [ ] **< 100ms** average test execution time
|
|
||||||
- [ ] **90%+ code coverage** for critical components
|
|
||||||
- [ ] **Zero flaky tests** in CI/CD pipeline
|
|
||||||
|
|
||||||
## Notes 📝
|
|
||||||
|
|
||||||
### Lessons Learned
|
|
||||||
- Vue 3 reactivity can be tricky with computed properties in tests
|
|
||||||
- Direct method calls work better than `trigger()` for complex events
|
|
||||||
- Mock levels provide excellent flexibility for different testing needs
|
|
||||||
- Behavior-focused testing is more maintainable than implementation-focused
|
|
||||||
|
|
||||||
### Technical Debt
|
|
||||||
- Some TypeScript linter errors in mock files (non-blocking)
|
|
||||||
- Event simulation needs better abstraction
|
|
||||||
- Test environment could be more robust
|
|
||||||
- Mock documentation could be more comprehensive
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: 2025-01-07*
|
|
||||||
*Status: Active development*
|
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
# Husky Conditional Activation System
|
|
||||||
|
|
||||||
**Author**: Matthew Raymer
|
|
||||||
**Date**: 2025-08-21T09:40Z
|
|
||||||
**Status**: 🎯 **ACTIVE** - Git hooks with optional activation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document describes the **conditional Husky activation system** implemented
|
|
||||||
in the TimeSafari project. The system provides standardized git hooks that are
|
|
||||||
committed to version control but only activate when explicitly enabled by
|
|
||||||
individual developers.
|
|
||||||
|
|
||||||
## Problem Statement
|
|
||||||
|
|
||||||
Traditional Husky implementations face several challenges:
|
|
||||||
|
|
||||||
1. **Automatic activation** on all systems can be disruptive
|
|
||||||
2. **Different environments** may have different requirements
|
|
||||||
3. **Team preferences** vary regarding git hook enforcement
|
|
||||||
4. **CI/CD systems** may not need or want git hooks
|
|
||||||
5. **New developers** may be surprised by unexpected hook behavior
|
|
||||||
|
|
||||||
## Solution: Conditional Activation
|
|
||||||
|
|
||||||
The conditional activation system solves these problems by:
|
|
||||||
|
|
||||||
- **Committing hooks to git** for consistency and version control
|
|
||||||
- **Making hooks optional** by default
|
|
||||||
- **Providing multiple activation methods** for flexibility
|
|
||||||
- **Ensuring hooks exit gracefully** when disabled
|
|
||||||
- **Maintaining team standards** without forcing compliance
|
|
||||||
|
|
||||||
## System Architecture
|
|
||||||
|
|
||||||
### **Core Components**
|
|
||||||
|
|
||||||
```
|
|
||||||
.husky/
|
|
||||||
├── _/husky.sh # Conditional activation logic
|
|
||||||
├── pre-commit # Pre-commit hook (linting)
|
|
||||||
├── commit-msg # Commit message validation
|
|
||||||
└── README.md # User activation instructions
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Activation Methods**
|
|
||||||
|
|
||||||
#### **Method 1: Environment Variable (Session Only)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export HUSKY_ENABLED=1
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Scope**: Current terminal session only
|
|
||||||
- **Use case**: Temporary activation for testing
|
|
||||||
- **Reset**: `unset HUSKY_ENABLED`
|
|
||||||
|
|
||||||
#### **Method 2: Local File (Persistent)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
touch .husky-enabled
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Scope**: Current repository, persistent
|
|
||||||
- **Use case**: Long-term activation for development
|
|
||||||
- **Reset**: `rm .husky-enabled`
|
|
||||||
|
|
||||||
#### **Method 3: Global Git Configuration**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git config --global husky.enabled true
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Scope**: All repositories for current user
|
|
||||||
- **Use case**: Developer preference across projects
|
|
||||||
- **Reset**: `git config --global --unset husky.enabled`
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### **Conditional Activation Logic**
|
|
||||||
|
|
||||||
The core logic in `.husky/_/husky.sh`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if Husky is enabled for this user
|
|
||||||
if [ "$HUSKY_ENABLED" != "1" ] && [ ! -f .husky-enabled ]; then
|
|
||||||
echo "Husky is not enabled. To enable:"
|
|
||||||
echo " export HUSKY_ENABLED=1"
|
|
||||||
echo " or create .husky-enabled file"
|
|
||||||
exit 0 # Graceful exit, not an error
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Hook Behavior**
|
|
||||||
|
|
||||||
When **disabled**:
|
|
||||||
|
|
||||||
- Hooks display helpful activation instructions
|
|
||||||
- Exit with code 0 (success, not error)
|
|
||||||
- No git operations are blocked
|
|
||||||
- No performance impact
|
|
||||||
|
|
||||||
When **enabled**:
|
|
||||||
|
|
||||||
- Hooks run normally with full functionality
|
|
||||||
- Standard Husky behavior applies
|
|
||||||
- Git operations may be blocked if hooks fail
|
|
||||||
|
|
||||||
## Available Hooks
|
|
||||||
|
|
||||||
### **Pre-commit Hook**
|
|
||||||
|
|
||||||
**File**: `.husky/pre-commit`
|
|
||||||
**Purpose**: Code quality enforcement before commits
|
|
||||||
**Action**: Runs `npm run lint-fix`
|
|
||||||
**When**: Before each commit
|
|
||||||
**Failure**: Prevents commit if linting fails
|
|
||||||
|
|
||||||
**Activation Check**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
if [ "$HUSKY_ENABLED" = "1" ] || [ -f .husky-enabled ]; then
|
|
||||||
echo "Running pre-commit hooks..."
|
|
||||||
npm run lint-fix
|
|
||||||
else
|
|
||||||
echo "Husky pre-commit hook skipped (not enabled)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Commit-msg Hook**
|
|
||||||
|
|
||||||
**File**: `.husky/commit-msg`
|
|
||||||
**Purpose**: Commit message format validation
|
|
||||||
**Action**: Runs `npx commitlint --edit "$1"`
|
|
||||||
**When**: After commit message is written
|
|
||||||
**Failure**: Prevents commit if message format is invalid
|
|
||||||
|
|
||||||
**Activation Check**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
if [ "$HUSKY_ENABLED" = "1" ] || [ -f .husky-enabled ]; then
|
|
||||||
echo "Running commit-msg hooks..."
|
|
||||||
npx commitlint --edit "$1"
|
|
||||||
else
|
|
||||||
echo "Husky commit-msg hook skipped (not enabled)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
## User Workflows
|
|
||||||
|
|
||||||
### **New Developer Setup**
|
|
||||||
|
|
||||||
1. **Clone repository**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd <repository-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Hooks are present but inactive**
|
|
||||||
- Pre-commit and commit-msg hooks exist
|
|
||||||
- No automatic activation
|
|
||||||
- Git operations work normally
|
|
||||||
|
|
||||||
3. **Optional: Enable hooks**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For current session only
|
|
||||||
export HUSKY_ENABLED=1
|
|
||||||
|
|
||||||
# For persistent activation
|
|
||||||
touch .husky-enabled
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Daily Development**
|
|
||||||
|
|
||||||
#### **With Hooks Disabled**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "feat: add new feature"
|
|
||||||
# Hooks are skipped, commit proceeds normally
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **With Hooks Enabled**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "feat: add new feature"
|
|
||||||
# Pre-commit hook runs linting
|
|
||||||
# Commit-msg hook validates message format
|
|
||||||
# Commit only proceeds if all hooks pass
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Troubleshooting**
|
|
||||||
|
|
||||||
#### **Hooks Not Running**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if hooks are enabled
|
|
||||||
echo $HUSKY_ENABLED
|
|
||||||
ls -la .husky-enabled
|
|
||||||
|
|
||||||
# Enable hooks
|
|
||||||
export HUSKY_ENABLED=1
|
|
||||||
# or
|
|
||||||
touch .husky-enabled
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Hooks Running Unexpectedly**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Disable hooks
|
|
||||||
unset HUSKY_ENABLED
|
|
||||||
rm -f .husky-enabled
|
|
||||||
|
|
||||||
# Check global configuration
|
|
||||||
git config --global --get husky.enabled
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Files
|
|
||||||
|
|
||||||
### **`.gitignore` Entry**
|
|
||||||
|
|
||||||
```gitignore
|
|
||||||
# Husky activation file (user-specific)
|
|
||||||
.husky-enabled
|
|
||||||
```
|
|
||||||
|
|
||||||
This ensures that:
|
|
||||||
|
|
||||||
- Hooks are committed to git (team standard)
|
|
||||||
- Activation files are not committed (user preference)
|
|
||||||
- Each developer can control their own activation
|
|
||||||
|
|
||||||
### **Package.json Dependencies**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"devDependencies": {
|
|
||||||
"husky": "^9.0.11",
|
|
||||||
"@commitlint/cli": "^18.6.1",
|
|
||||||
"@commitlint/config-conventional": "^18.6.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
### **For Development Teams**
|
|
||||||
|
|
||||||
1. **Consistency**: All developers have the same hook configuration
|
|
||||||
2. **Flexibility**: Individual developers can choose activation
|
|
||||||
3. **Standards**: Team coding standards are enforced when enabled
|
|
||||||
4. **Version Control**: Hook configuration is tracked and versioned
|
|
||||||
5. **Onboarding**: New developers get standardized setup
|
|
||||||
|
|
||||||
### **For Individual Developers**
|
|
||||||
|
|
||||||
1. **Choice**: Control over when hooks are active
|
|
||||||
2. **Performance**: No unnecessary hook execution when disabled
|
|
||||||
3. **Learning**: Gradual adoption of git hook practices
|
|
||||||
4. **Debugging**: Easy to disable hooks for troubleshooting
|
|
||||||
5. **Environment**: Works across different development environments
|
|
||||||
|
|
||||||
### **For CI/CD Systems**
|
|
||||||
|
|
||||||
1. **No Interference**: Hooks don't run in automated environments
|
|
||||||
2. **Consistency**: Same hook logic available if needed
|
|
||||||
3. **Flexibility**: Can enable hooks in specific CI scenarios
|
|
||||||
4. **Reliability**: No unexpected hook failures in automation
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### **Team Adoption**
|
|
||||||
|
|
||||||
1. **Start with disabled hooks** for new team members
|
|
||||||
2. **Encourage gradual adoption** of hook activation
|
|
||||||
3. **Document hook benefits** and usage patterns
|
|
||||||
4. **Provide training** on hook configuration
|
|
||||||
5. **Support troubleshooting** when hooks cause issues
|
|
||||||
|
|
||||||
### **Hook Development**
|
|
||||||
|
|
||||||
1. **Keep hooks lightweight** and fast
|
|
||||||
2. **Provide clear error messages** when hooks fail
|
|
||||||
3. **Include helpful activation instructions** in disabled state
|
|
||||||
4. **Test hooks in both enabled and disabled states**
|
|
||||||
5. **Document hook requirements** and dependencies
|
|
||||||
|
|
||||||
### **Configuration Management**
|
|
||||||
|
|
||||||
1. **Commit hook files** to version control
|
|
||||||
2. **Ignore activation files** in .gitignore
|
|
||||||
3. **Document activation methods** clearly
|
|
||||||
4. **Provide examples** for common use cases
|
|
||||||
5. **Maintain backward compatibility** when updating hooks
|
|
||||||
|
|
||||||
## Troubleshooting Guide
|
|
||||||
|
|
||||||
### **Common Issues**
|
|
||||||
|
|
||||||
#### **Hooks Running When Not Expected**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check all activation methods
|
|
||||||
echo "Environment variable: $HUSKY_ENABLED"
|
|
||||||
echo "Local file exists: $([ -f .husky-enabled ] && echo "yes" || echo "no")"
|
|
||||||
echo "Global config: $(git config --global --get husky.enabled)"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Hooks Not Running When Expected**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verify hook files exist and are executable
|
|
||||||
ls -la .husky/
|
|
||||||
chmod +x .husky/pre-commit
|
|
||||||
chmod +x .husky/commit-msg
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Permission Denied Errors**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Fix file permissions
|
|
||||||
chmod +x .husky/_/husky.sh
|
|
||||||
chmod +x .husky/pre-commit
|
|
||||||
chmod +x .husky/commit-msg
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Debug Mode**
|
|
||||||
|
|
||||||
Enable debug output to troubleshoot hook issues:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export HUSKY_DEBUG=1
|
|
||||||
export HUSKY_ENABLED=1
|
|
||||||
git commit -m "test: debug commit"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### **Planned Improvements**
|
|
||||||
|
|
||||||
1. **Hook Configuration File**: YAML/JSON configuration for hook behavior
|
|
||||||
2. **Selective Hook Activation**: Enable/disable specific hooks individually
|
|
||||||
3. **Hook Performance Metrics**: Track execution time and success rates
|
|
||||||
4. **Integration with IDEs**: IDE-specific activation methods
|
|
||||||
5. **Remote Configuration**: Team-wide hook settings via configuration
|
|
||||||
|
|
||||||
### **Extension Points**
|
|
||||||
|
|
||||||
1. **Custom Hook Scripts**: Easy addition of project-specific hooks
|
|
||||||
2. **Hook Templates**: Reusable hook patterns for common tasks
|
|
||||||
3. **Conditional Logic**: Complex activation rules based on context
|
|
||||||
4. **Notification System**: Hook status reporting and alerts
|
|
||||||
5. **Analytics**: Hook usage and effectiveness tracking
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The conditional Husky activation system provides an elegant solution to the
|
|
||||||
challenges of git hook management in team environments. By committing
|
|
||||||
standardized hooks while making activation optional, it balances consistency
|
|
||||||
with flexibility, enabling teams to maintain coding standards without forcing compliance.
|
|
||||||
|
|
||||||
This approach supports gradual adoption, respects individual preferences, and
|
|
||||||
provides a solid foundation for git hook practices that can evolve with team needs
|
|
||||||
and project requirements.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Related Documents**:
|
|
||||||
|
|
||||||
- [Git Hooks Best Practices](./git-hooks-best-practices.md)
|
|
||||||
- [Code Quality Standards](./code-quality-standards.md)
|
|
||||||
- [Development Workflow](./development-workflow.md)
|
|
||||||
|
|
||||||
**Maintainer**: Development Team
|
|
||||||
**Review Schedule**: Quarterly
|
|
||||||
**Next Review**: 2025-11-21
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export default {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
moduleFileExtensions: ['ts', 'js', 'json', 'vue'],
|
moduleFileExtensions: ['ts', 'js', 'json', 'vue'],
|
||||||
2637
package-lock.json
generated
2637
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -12,10 +12,6 @@
|
|||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
||||||
"test:prerequisites": "node scripts/check-prerequisites.js",
|
"test:prerequisites": "node scripts/check-prerequisites.js",
|
||||||
"test": "vitest",
|
|
||||||
"test:unit": "vitest --run",
|
|
||||||
"test:unit:watch": "vitest --watch",
|
|
||||||
"test:unit:coverage": "vitest --coverage --run",
|
|
||||||
"check:dependencies": "./scripts/check-dependencies.sh",
|
"check:dependencies": "./scripts/check-dependencies.sh",
|
||||||
"test:all": "npm run lint && tsc && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
|
"test:all": "npm run lint && tsc && npm run test:web && npm run test:mobile && ./scripts/test-safety-check.sh && echo '\n\n\nGotta add the performance tests'",
|
||||||
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
||||||
@@ -139,9 +135,7 @@
|
|||||||
"*.{js,ts,vue,css,md,json,yml,yaml}": "eslint --fix || true"
|
"*.{js,ts,vue,css,md,json,yml,yaml}": "eslint --fix || true"
|
||||||
},
|
},
|
||||||
"commitlint": {
|
"commitlint": {
|
||||||
"extends": [
|
"extends": ["@commitlint/config-conventional"]
|
||||||
"@commitlint/config-conventional"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/electron": "^5.0.1",
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
@@ -232,8 +226,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@commitlint/cli": "^18.6.1",
|
|
||||||
"@commitlint/config-conventional": "^18.6.2",
|
|
||||||
"@playwright/test": "^1.54.2",
|
"@playwright/test": "^1.54.2",
|
||||||
"@types/dom-webcodecs": "^0.1.7",
|
"@types/dom-webcodecs": "^0.1.7",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
@@ -249,9 +241,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vitest/coverage-v8": "^2.1.9",
|
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/test-utils": "^2.4.4",
|
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"better-sqlite3-multiple-ciphers": "^12.1.1",
|
"better-sqlite3-multiple-ciphers": "^12.1.1",
|
||||||
"browserify-fs": "^1.0.0",
|
"browserify-fs": "^1.0.0",
|
||||||
@@ -263,12 +253,13 @@
|
|||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"husky": "^9.1.7",
|
|
||||||
"jest": "^30.0.4",
|
"jest": "^30.0.4",
|
||||||
"jsdom": "^24.0.0",
|
|
||||||
"lint-staged": "^15.2.2",
|
|
||||||
"markdownlint": "^0.37.4",
|
"markdownlint": "^0.37.4",
|
||||||
"markdownlint-cli": "^0.44.0",
|
"markdownlint-cli": "^0.44.0",
|
||||||
|
"husky": "^9.0.11",
|
||||||
|
"lint-staged": "^15.2.2",
|
||||||
|
"@commitlint/cli": "^18.6.1",
|
||||||
|
"@commitlint/config-conventional": "^18.6.2",
|
||||||
"npm-check-updates": "^17.1.13",
|
"npm-check-updates": "^17.1.13",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
@@ -278,7 +269,6 @@
|
|||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.0",
|
||||||
"tsx": "^4.20.4",
|
"tsx": "^4.20.4",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0"
|
||||||
"vitest": "^2.1.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
@@ -14,4 +14,12 @@
|
|||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
background-color: #FFF !important;
|
background-color: #FFF !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-overlay {
|
||||||
|
@apply z-50 fixed inset-0 bg-black/50 flex justify-center items-center p-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
@apply bg-white p-4 rounded-lg w-full max-w-lg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- similar to UserNameDialog -->
|
<!-- similar to UserNameDialog -->
|
||||||
<template>
|
<template>
|
||||||
<div v-if="visible" :class="overlayClasses">
|
<div v-if="visible" class="dialog-overlay">
|
||||||
<div :class="dialogClasses">
|
<div class="dialog">
|
||||||
<h1 :class="titleClasses">{{ title }}</h1>
|
<h1 :class="titleClasses">{{ title }}</h1>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
Note that their name is only stored on this device.
|
Note that their name is only stored on this device.
|
||||||
@@ -61,20 +61,6 @@ export default class ContactNameDialog extends Vue {
|
|||||||
title = "Contact Name";
|
title = "Contact Name";
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* CSS classes for the modal overlay backdrop
|
|
||||||
*/
|
|
||||||
get overlayClasses(): string {
|
|
||||||
return "z-index-50 fixed top-0 left-0 right-0 bottom-0 bg-black/50 flex justify-center items-center p-6";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CSS classes for the modal dialog container
|
|
||||||
*/
|
|
||||||
get dialogClasses(): string {
|
|
||||||
return "bg-white p-4 rounded-lg w-full max-w-[500px]";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSS classes for the dialog title
|
* CSS classes for the dialog title
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -212,30 +212,8 @@ export default class FeedFilters extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#dialogFeedFilters.dialog-overlay {
|
#dialogFeedFilters.dialog-overlay {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -665,27 +665,3 @@ export default class GiftedDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -291,27 +291,3 @@ export default class GivenPrompts extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -931,32 +931,6 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 700px;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add styles for diagnostic panel */
|
/* Add styles for diagnostic panel */
|
||||||
.diagnostic-panel {
|
.diagnostic-panel {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
|||||||
@@ -93,27 +93,3 @@ export default class InviteDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -312,28 +312,3 @@ export default class OfferDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.dialog-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background: white;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
max-width: 500px;
|
|
||||||
width: 90%;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -307,27 +307,3 @@ export default class OnboardingDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 40;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -628,34 +628,6 @@ export default class PhotoDialog extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Dialog overlay styling */
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 60;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dialog container styling */
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 700px;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Camera preview styling */
|
/* Camera preview styling */
|
||||||
.camera-preview {
|
.camera-preview {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -134,27 +134,3 @@ export default class UserNameDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,706 +0,0 @@
|
|||||||
import { describe, it, expect, beforeEach } 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 meet WCAG accessibility standards", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const container = wrapper.find(".mt-2");
|
|
||||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
|
||||||
const button = wrapper.find("button");
|
|
||||||
|
|
||||||
// Semantic structure
|
|
||||||
expect(container.exists()).toBe(true);
|
|
||||||
expect(checkbox.exists()).toBe(true);
|
|
||||||
expect(button.exists()).toBe(true);
|
|
||||||
|
|
||||||
// Form control accessibility
|
|
||||||
expect(checkbox.attributes("type")).toBe("checkbox");
|
|
||||||
expect(checkbox.attributes("data-testid")).toBe("contactCheckAllBottom");
|
|
||||||
expect(button.text()).toBe("Copy");
|
|
||||||
|
|
||||||
// Note: Component has good accessibility but could be enhanced with:
|
|
||||||
// - aria-label for checkbox, aria-describedby for button
|
|
||||||
});
|
|
||||||
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support keyboard navigation", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
|
||||||
const button = wrapper.find("button");
|
|
||||||
|
|
||||||
// Test that controls are clickable (supports keyboard navigation)
|
|
||||||
expect(checkbox.exists()).toBe(true);
|
|
||||||
expect(button.exists()).toBe(true);
|
|
||||||
|
|
||||||
// Note: Component doesn't have explicit keyboard event handlers
|
|
||||||
// Keyboard navigation would be handled by browser defaults
|
|
||||||
// Test that controls are clickable (which supports keyboard navigation)
|
|
||||||
checkbox.trigger("click");
|
|
||||||
expect(wrapper.emitted("toggle-all-selection")).toBeTruthy();
|
|
||||||
|
|
||||||
button.trigger("click");
|
|
||||||
expect(wrapper.emitted("copy-selected")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper ARIA attributes", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
|
||||||
|
|
||||||
// Verify accessibility attributes
|
|
||||||
expect(checkbox.attributes("data-testid")).toBe("contactCheckAllBottom");
|
|
||||||
|
|
||||||
// Note: Could be enhanced with aria-label, aria-describedby
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should maintain accessibility 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);
|
|
||||||
|
|
||||||
if (!props.showGiveNumbers) {
|
|
||||||
// Controls should be accessible when rendered
|
|
||||||
const checkbox = testWrapper.find('input[type="checkbox"]');
|
|
||||||
const button = testWrapper.find("button");
|
|
||||||
|
|
||||||
expect(checkbox.exists()).toBe(true);
|
|
||||||
expect(checkbox.attributes("type")).toBe("checkbox");
|
|
||||||
expect(checkbox.attributes("data-testid")).toBe(
|
|
||||||
"contactCheckAllBottom",
|
|
||||||
);
|
|
||||||
expect(button.exists()).toBe(true);
|
|
||||||
expect(button.text()).toBe("Copy");
|
|
||||||
} else {
|
|
||||||
// Controls should not render when showGiveNumbers is true
|
|
||||||
expect(testWrapper.find('input[type="checkbox"]').exists()).toBe(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
expect(testWrapper.find("button").exists()).toBe(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have sufficient color contrast", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const container = wrapper.find(".mt-2");
|
|
||||||
|
|
||||||
// Verify container has proper styling
|
|
||||||
expect(container.classes()).toContain("mt-2");
|
|
||||||
expect(container.classes()).toContain("w-full");
|
|
||||||
expect(container.classes()).toContain("text-left");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have descriptive content", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const button = wrapper.find("button");
|
|
||||||
|
|
||||||
// Button should have descriptive text
|
|
||||||
expect(button.exists()).toBe(true);
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Error Handling", () => {
|
|
||||||
it("should handle null props gracefully", () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
showGiveNumbers: null as any,
|
|
||||||
allContactsSelected: null as any,
|
|
||||||
copyButtonClass: null as any,
|
|
||||||
copyButtonDisabled: null as any,
|
|
||||||
});
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle undefined props gracefully", () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
showGiveNumbers: undefined as any,
|
|
||||||
allContactsSelected: undefined as any,
|
|
||||||
copyButtonClass: undefined as any,
|
|
||||||
copyButtonDisabled: undefined as any,
|
|
||||||
});
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle malformed props without crashing", () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
showGiveNumbers: "invalid" as any,
|
|
||||||
allContactsSelected: "invalid" as any,
|
|
||||||
copyButtonClass: 123 as any,
|
|
||||||
copyButtonDisabled: "invalid" as any,
|
|
||||||
});
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle rapid prop changes without errors", async () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
|
|
||||||
// Rapidly change props
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
await wrapper.setProps({
|
|
||||||
showGiveNumbers: i % 2 === 0,
|
|
||||||
allContactsSelected: i % 3 === 0,
|
|
||||||
copyButtonClass: `class-${i}`,
|
|
||||||
copyButtonDisabled: i % 4 === 0,
|
|
||||||
});
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Performance Testing", () => {
|
|
||||||
it("should render within acceptable time", () => {
|
|
||||||
const start = performance.now();
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const end = performance.now();
|
|
||||||
|
|
||||||
expect(end - start).toBeLessThan(50); // 50ms threshold
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle rapid prop changes efficiently", async () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const start = performance.now();
|
|
||||||
|
|
||||||
// Rapidly change props
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
await wrapper.setProps({
|
|
||||||
showGiveNumbers: i % 2 === 0,
|
|
||||||
allContactsSelected: i % 2 === 0,
|
|
||||||
});
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
}
|
|
||||||
|
|
||||||
const end = performance.now();
|
|
||||||
expect(end - start).toBeLessThan(1000); // 1 second threshold
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not cause memory leaks with button interactions", async () => {
|
|
||||||
// Create and destroy multiple components
|
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
const tempWrapper = mountComponent();
|
|
||||||
const button = tempWrapper.find("button");
|
|
||||||
if (button.exists() && !button.attributes("disabled")) {
|
|
||||||
await button.trigger("click");
|
|
||||||
}
|
|
||||||
tempWrapper.unmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force garbage collection if available
|
|
||||||
if (global.gc) {
|
|
||||||
global.gc();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify component cleanup
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Integration Testing", () => {
|
|
||||||
it("should work with parent component context", () => {
|
|
||||||
// Mock parent component
|
|
||||||
const ParentComponent = {
|
|
||||||
template: `
|
|
||||||
<div>
|
|
||||||
<ContactBulkActions
|
|
||||||
:showGiveNumbers="showGiveNumbers"
|
|
||||||
:allContactsSelected="allContactsSelected"
|
|
||||||
:copyButtonClass="copyButtonClass"
|
|
||||||
:copyButtonDisabled="copyButtonDisabled"
|
|
||||||
@toggle-all-selection="handleToggleAll"
|
|
||||||
@copy-selected="handleCopySelected"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
components: { ContactBulkActions },
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showGiveNumbers: false,
|
|
||||||
allContactsSelected: false,
|
|
||||||
copyButtonClass: "btn-primary",
|
|
||||||
copyButtonDisabled: false,
|
|
||||||
toggleCalled: false,
|
|
||||||
copyCalled: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleToggleAll() {
|
|
||||||
(this as any).toggleCalled = true;
|
|
||||||
},
|
|
||||||
handleCopySelected() {
|
|
||||||
(this as any).copyCalled = true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const parentWrapper = mount(ParentComponent);
|
|
||||||
const bulkActions = parentWrapper.findComponent(ContactBulkActions);
|
|
||||||
|
|
||||||
expect(bulkActions.exists()).toBe(true);
|
|
||||||
expect((parentWrapper.vm as any).toggleCalled).toBe(false);
|
|
||||||
expect((parentWrapper.vm as any).copyCalled).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should integrate with contact service", () => {
|
|
||||||
// Mock contact service
|
|
||||||
const contactService = {
|
|
||||||
getSelectedContacts: vi.fn().mockReturnValue([]),
|
|
||||||
toggleAllSelection: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper = mountComponent({
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
contactService,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
expect(contactService.getSelectedContacts).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with global properties", () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
global: {
|
|
||||||
config: {
|
|
||||||
globalProperties: {
|
|
||||||
$t: (key: string) => key,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Snapshot Testing", () => {
|
|
||||||
it("should maintain consistent DOM structure", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const html = wrapper.html();
|
|
||||||
|
|
||||||
// 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"]');
|
|
||||||
expect(checkbox.exists()).toBe(true);
|
|
||||||
expect(checkbox.attributes("data-testid")).toBe("contactCheckAllBottom");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have consistent CSS classes", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const container = wrapper.find(".mt-2");
|
|
||||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
|
||||||
|
|
||||||
// Verify container classes
|
|
||||||
const expectedContainerClasses = ["mt-2", "w-full", "text-left"];
|
|
||||||
|
|
||||||
expectedContainerClasses.forEach((className) => {
|
|
||||||
expect(container.classes()).toContain(className);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify checkbox classes
|
|
||||||
const expectedCheckboxClasses = ["align-middle", "ml-2", "h-6", "w-6"];
|
|
||||||
|
|
||||||
expectedCheckboxClasses.forEach((className) => {
|
|
||||||
expect(checkbox.classes()).toContain(className);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should maintain accessibility structure", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const container = wrapper.find(".mt-2");
|
|
||||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
|
||||||
const button = wrapper.find("button");
|
|
||||||
|
|
||||||
// Verify basic structure
|
|
||||||
expect(container.exists()).toBe(true);
|
|
||||||
expect(checkbox.exists()).toBe(true);
|
|
||||||
expect(button.exists()).toBe(true);
|
|
||||||
|
|
||||||
// Verify accessibility attributes
|
|
||||||
expect(checkbox.attributes("data-testid")).toBe("contactCheckAllBottom");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,542 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 } 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: '<div class="entity-icon-stub">EntityIcon</div>',
|
|
||||||
props: ["contact", "iconSize"],
|
|
||||||
},
|
|
||||||
"font-awesome": {
|
|
||||||
template: '<span class="font-awesome-stub">FontAwesome</span>',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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:other" });
|
|
||||||
wrapper = mountComponent({
|
|
||||||
showActions: true,
|
|
||||||
contact,
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.find('[data-testid="offerButton"]').trigger("click");
|
|
||||||
|
|
||||||
expect(wrapper.emitted("open-offer-dialog")).toBeTruthy();
|
|
||||||
// Test that both parameters are emitted correctly
|
|
||||||
const emittedData = wrapper.emitted("open-offer-dialog")[0];
|
|
||||||
expect(emittedData).toEqual(["did:ethr:test:other", contact.name]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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(/<li[^>]*class="[^"]*border-b[^"]*"[^>]*>/);
|
|
||||||
expect(html).toMatch(/<div[^>]*class="[^"]*flex[^"]*"[^>]*>/);
|
|
||||||
expect(html).toContain("EntityIcon");
|
|
||||||
expect(html).toContain('data-testid="contactListItem"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should maintain consistent structure with different prop combinations", () => {
|
|
||||||
const propCombinations = [
|
|
||||||
{ showCheckbox: true, showActions: false },
|
|
||||||
{ showCheckbox: false, showActions: true },
|
|
||||||
{ showCheckbox: true, showActions: true },
|
|
||||||
{ showCheckbox: false, showActions: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
propCombinations.forEach((props) => {
|
|
||||||
const testWrapper = mountComponent(props);
|
|
||||||
const html = testWrapper.html();
|
|
||||||
|
|
||||||
expect(html).toMatch(/<li[^>]*class="[^"]*border-b[^"]*"[^>]*>/);
|
|
||||||
expect(html).toContain("EntityIcon");
|
|
||||||
|
|
||||||
if (props.showCheckbox) {
|
|
||||||
expect(html).toContain('data-testid="contactCheckOne"');
|
|
||||||
} else {
|
|
||||||
expect(html).not.toContain('data-testid="contactCheckOne"');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Accessibility Testing", () => {
|
|
||||||
it("should meet WCAG accessibility standards", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const listItem = wrapper.find('[data-testid="contactListItem"]');
|
|
||||||
const checkbox = wrapper.find('[data-testid="contactCheckOne"]');
|
|
||||||
const offerButton = wrapper.find('[data-testid="offerButton"]');
|
|
||||||
|
|
||||||
// Semantic structure
|
|
||||||
expect(listItem.exists()).toBe(true);
|
|
||||||
expect(listItem.element.tagName.toLowerCase()).toBe("li");
|
|
||||||
|
|
||||||
// Form control accessibility
|
|
||||||
if (checkbox.exists()) {
|
|
||||||
expect(checkbox.attributes("type")).toBe("checkbox");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Button accessibility
|
|
||||||
if (offerButton.exists()) {
|
|
||||||
expect(offerButton.text()).toBe("Offer");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support keyboard navigation", () => {
|
|
||||||
wrapper = mountComponent({ showCheckbox: true, showActions: true });
|
|
||||||
|
|
||||||
const checkbox = wrapper.find('[data-testid="contactCheckOne"]');
|
|
||||||
const offerButton = wrapper.find('[data-testid="offerButton"]');
|
|
||||||
|
|
||||||
// Test that controls are clickable (supports keyboard navigation)
|
|
||||||
expect(checkbox.exists()).toBe(true);
|
|
||||||
expect(offerButton.exists()).toBe(true);
|
|
||||||
|
|
||||||
checkbox.trigger("click");
|
|
||||||
expect(wrapper.emitted("toggle-selection")).toBeTruthy();
|
|
||||||
|
|
||||||
offerButton.trigger("click");
|
|
||||||
expect(wrapper.emitted("open-offer-dialog")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have descriptive content", () => {
|
|
||||||
const contact = createStandardMockContact({ name: "Test Contact" });
|
|
||||||
wrapper = mountComponent({ contact });
|
|
||||||
|
|
||||||
expect(wrapper.text().replace(/\u00A0/g, " ")).toContain("Test Contact");
|
|
||||||
expect(wrapper.text()).toContain("did:ethr:test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should maintain accessibility with different prop combinations", () => {
|
|
||||||
const testCases = [
|
|
||||||
{ showCheckbox: true, showActions: false },
|
|
||||||
{ showCheckbox: false, showActions: true },
|
|
||||||
{ showCheckbox: true, showActions: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach((props) => {
|
|
||||||
const testWrapper = mountComponent(props);
|
|
||||||
const listItem = testWrapper.find('[data-testid="contactListItem"]');
|
|
||||||
|
|
||||||
expect(listItem.exists()).toBe(true);
|
|
||||||
expect(testWrapper.find(".entity-icon-stub").exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Centralized Utility Testing", () => {
|
|
||||||
it("should use centralized component wrapper", () => {
|
|
||||||
const wrapperFactory = createComponentWrapper(ContactListItem, {
|
|
||||||
contact: createStandardMockContact(),
|
|
||||||
activeDid: "did:ethr:test:active",
|
|
||||||
showCheckbox: false,
|
|
||||||
showActions: false,
|
|
||||||
isSelected: false,
|
|
||||||
showGiveTotals: true,
|
|
||||||
showGiveConfirmed: true,
|
|
||||||
givenToMeDescriptions: {},
|
|
||||||
givenToMeConfirmed: {},
|
|
||||||
givenToMeUnconfirmed: {},
|
|
||||||
givenByMeDescriptions: {},
|
|
||||||
givenByMeConfirmed: {},
|
|
||||||
givenByMeUnconfirmed: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const testWrapper = wrapperFactory();
|
|
||||||
expect(testWrapper.exists()).toBe(true);
|
|
||||||
expect(testWrapper.find('[data-testid="contactListItem"]').exists()).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should test 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 list item",
|
|
||||||
test: (wrapper: any) =>
|
|
||||||
wrapper.find('[data-testid="contactListItem"]').exists(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "has entity icon",
|
|
||||||
test: (wrapper: any) => wrapper.find(".entity-icon-stub").exists(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "has contact name",
|
|
||||||
test: (wrapper: any) => wrapper.find("h2").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({ isSelected: "invalid" as any });
|
|
||||||
},
|
|
||||||
expectedBehavior: "should handle gracefully",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const results = await testErrorHandling(wrapper, errorScenarios);
|
|
||||||
expect(results).toHaveLength(1);
|
|
||||||
expect(results.every((r) => r.success)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,559 +0,0 @@
|
|||||||
/**
|
|
||||||
* ImageViewer Mock Units Tests
|
|
||||||
*
|
|
||||||
* Comprehensive behavior-focused tests for the ImageViewer mock units.
|
|
||||||
* Tests cover mock functionality, platform detection, share features,
|
|
||||||
* error handling, and accessibility across different scenarios.
|
|
||||||
*
|
|
||||||
* Test Categories:
|
|
||||||
* - Component Rendering & Props
|
|
||||||
* - Platform Detection (Mobile vs Desktop)
|
|
||||||
* - Share Functionality (Success, Fallback, Error)
|
|
||||||
* - Image Loading & Error Handling
|
|
||||||
* - Accessibility & User Experience
|
|
||||||
* - Performance & Transitions
|
|
||||||
*
|
|
||||||
* @author Matthew Raymer
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
||||||
import { mount, VueWrapper } from "@vue/test-utils";
|
|
||||||
import {
|
|
||||||
createImageViewerMockWrapper,
|
|
||||||
createImageViewerTestScenarios,
|
|
||||||
createMockImageData,
|
|
||||||
createMockUserAgent,
|
|
||||||
createMockNavigator,
|
|
||||||
createMockWindow,
|
|
||||||
createSimpleImageViewerMock,
|
|
||||||
createStandardImageViewerMock,
|
|
||||||
createComplexImageViewerMock,
|
|
||||||
createIntegrationImageViewerMock,
|
|
||||||
} from "./__mocks__/ImageViewer.mock";
|
|
||||||
|
|
||||||
describe("ImageViewer Mock Units", () => {
|
|
||||||
let wrapper: VueWrapper<any>;
|
|
||||||
let mockNavigator: any;
|
|
||||||
let mockWindow: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Setup global mocks
|
|
||||||
mockNavigator = createMockNavigator();
|
|
||||||
mockWindow = createMockWindow();
|
|
||||||
|
|
||||||
// Mock global objects
|
|
||||||
global.navigator = mockNavigator;
|
|
||||||
global.window = mockWindow;
|
|
||||||
|
|
||||||
// Reset mocks
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
if (wrapper) {
|
|
||||||
wrapper.unmount();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Mock Levels", () => {
|
|
||||||
it("simple mock provides basic functionality", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("simple");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
expect(wrapper.find(".image-viewer-mock").exists()).toBe(true);
|
|
||||||
expect(wrapper.find(".mock-overlay").exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("standard mock provides realistic behavior", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
expect(wrapper.find('[data-testid="close-button"]').exists()).toBe(true);
|
|
||||||
expect(wrapper.find('[data-testid="viewer-image"]').exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("complex mock provides error handling", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("complex");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).imageError).toBeDefined();
|
|
||||||
expect((wrapper.vm as any).loadAttempts).toBeDefined();
|
|
||||||
expect((wrapper.vm as any).canRetry).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("integration mock provides analytics", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("integration");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).getAnalytics).toBeDefined();
|
|
||||||
const analytics = (wrapper.vm as any).getAnalytics();
|
|
||||||
expect(analytics.openCount).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Component Rendering & Props", () => {
|
|
||||||
it("renders with basic props", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("simple");
|
|
||||||
wrapper = createWrapper(createMockImageData());
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
expect(wrapper.find(".image-viewer-mock").exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders with standard props", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
expect(wrapper.find('[data-testid="close-button"]').exists()).toBe(true);
|
|
||||||
expect(wrapper.find('[data-testid="viewer-image"]').exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles required props correctly", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
const requiredProps = {
|
|
||||||
imageUrl: "https://example.com/test.jpg",
|
|
||||||
isOpen: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper = createWrapper(requiredProps);
|
|
||||||
|
|
||||||
expect(wrapper.props("imageUrl")).toBe(requiredProps.imageUrl);
|
|
||||||
expect(wrapper.props("isOpen")).toBe(requiredProps.isOpen);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits close event when close button clicked", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).close();
|
|
||||||
|
|
||||||
expect(wrapper.emitted("update:isOpen")).toBeTruthy();
|
|
||||||
expect(wrapper.emitted("update:isOpen")?.[0]).toEqual([false]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits close event when image clicked", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).close();
|
|
||||||
|
|
||||||
expect(wrapper.emitted("update:isOpen")).toBeTruthy();
|
|
||||||
expect(wrapper.emitted("update:isOpen")?.[0]).toEqual([false]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Platform Detection", () => {
|
|
||||||
it.skip("shows share button on mobile platforms", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
const mobileProps = createMockImageData({ isOpen: true });
|
|
||||||
|
|
||||||
wrapper = createWrapper(mobileProps);
|
|
||||||
|
|
||||||
// Create a new wrapper with mobile user agent
|
|
||||||
const mobileWrapper = createWrapper(mobileProps);
|
|
||||||
(mobileWrapper.vm as any).userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "iOS" })
|
|
||||||
});
|
|
||||||
|
|
||||||
expect((mobileWrapper.vm as any).isMobile).toBe(true);
|
|
||||||
expect(mobileWrapper.find('[data-testid="share-button"]').exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hides share button on desktop platforms", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
const desktopProps = createMockImageData({ isOpen: true });
|
|
||||||
|
|
||||||
wrapper = createWrapper(desktopProps);
|
|
||||||
|
|
||||||
// Mock desktop user agent
|
|
||||||
(wrapper.vm as any).userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "Windows" })
|
|
||||||
});
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).isMobile).toBe(false);
|
|
||||||
expect(wrapper.find('[data-testid="share-button"]').exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects iOS platform correctly", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Mock iOS user agent
|
|
||||||
(wrapper.vm as any).userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "iOS" })
|
|
||||||
});
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).isMobile).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects Android platform correctly", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Mock Android user agent
|
|
||||||
(wrapper.vm as any).userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "Android" })
|
|
||||||
});
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).isMobile).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects desktop platforms correctly", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Mock desktop user agent
|
|
||||||
(wrapper.vm as any).userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "Windows" })
|
|
||||||
});
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).isMobile).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Share Functionality", () => {
|
|
||||||
it("calls navigator.share on mobile with share API", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Mock mobile user agent
|
|
||||||
(wrapper.vm as any).userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "iOS" })
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock navigator.share
|
|
||||||
const mockShare = vi.fn().mockResolvedValue(undefined);
|
|
||||||
Object.defineProperty(global, 'navigator', {
|
|
||||||
value: { share: mockShare },
|
|
||||||
writable: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).handleShare();
|
|
||||||
|
|
||||||
expect(mockShare).toHaveBeenCalledWith({
|
|
||||||
url: "https://example.com/test-image.jpg"
|
|
||||||
});
|
|
||||||
expect((wrapper.vm as any).shareSuccess).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("falls back to window.open when share API unavailable", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Mock mobile user agent
|
|
||||||
(wrapper.vm as any).userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "iOS" })
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock window.open
|
|
||||||
const mockOpen = vi.fn();
|
|
||||||
Object.defineProperty(global, 'window', {
|
|
||||||
value: { open: mockOpen },
|
|
||||||
writable: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove navigator.share
|
|
||||||
Object.defineProperty(global, 'navigator', {
|
|
||||||
value: {},
|
|
||||||
writable: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).handleShare();
|
|
||||||
|
|
||||||
expect(mockOpen).toHaveBeenCalledWith(
|
|
||||||
"https://example.com/test-image.jpg",
|
|
||||||
"_blank"
|
|
||||||
);
|
|
||||||
expect((wrapper.vm as any).shareSuccess).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles share API errors gracefully", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Mock mobile user agent
|
|
||||||
(wrapper.vm as any).userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "iOS" })
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock navigator.share to throw error
|
|
||||||
const mockShare = vi.fn().mockRejectedValue(new Error("Share failed"));
|
|
||||||
const mockOpen = vi.fn();
|
|
||||||
|
|
||||||
Object.defineProperty(global, 'navigator', {
|
|
||||||
value: { share: mockShare },
|
|
||||||
writable: true
|
|
||||||
});
|
|
||||||
Object.defineProperty(global, 'window', {
|
|
||||||
value: { open: mockOpen },
|
|
||||||
writable: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).handleShare();
|
|
||||||
|
|
||||||
expect(mockShare).toHaveBeenCalled();
|
|
||||||
expect(mockOpen).toHaveBeenCalledWith(
|
|
||||||
"https://example.com/test-image.jpg",
|
|
||||||
"_blank"
|
|
||||||
);
|
|
||||||
expect((wrapper.vm as any).shareSuccess).toBe(true);
|
|
||||||
expect((wrapper.vm as any).shareError).toBeInstanceOf(Error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not show share button on desktop", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Mock desktop user agent
|
|
||||||
(wrapper.vm as any).userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "Windows" })
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.find('[data-testid="share-button"]').exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tracks share analytics correctly", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("integration");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Mock mobile user agent
|
|
||||||
(wrapper.vm as any).userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "iOS" })
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).handleShare();
|
|
||||||
|
|
||||||
const analytics = (wrapper.vm as any).getAnalytics();
|
|
||||||
expect(analytics.shareCount).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Image Loading & Error Handling", () => {
|
|
||||||
it("handles image load events", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("complex");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).handleImageLoad();
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).imageLoaded).toBe(true);
|
|
||||||
expect((wrapper.vm as any).imageError).toBe(false);
|
|
||||||
expect(wrapper.emitted("image-load")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles image error events", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("complex");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).handleImageError();
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).imageError).toBe(true);
|
|
||||||
expect((wrapper.vm as any).imageLoaded).toBe(false);
|
|
||||||
expect((wrapper.vm as any).loadAttempts).toBe(1);
|
|
||||||
expect(wrapper.emitted("image-error")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows error state when image fails to load", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("complex");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).handleImageError();
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).imageError).toBe(true);
|
|
||||||
expect(wrapper.find('[data-testid="viewer-image"]').exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows retrying failed image loads", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("complex");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Trigger error first
|
|
||||||
await (wrapper.vm as any).handleImageError();
|
|
||||||
expect((wrapper.vm as any).imageError).toBe(true);
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).retryImage();
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).imageError).toBe(false);
|
|
||||||
expect((wrapper.vm as any).imageLoaded).toBe(false);
|
|
||||||
expect((wrapper.vm as any).loadAttempts).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("limits retry attempts", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("complex");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Trigger errors multiple times
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
await (wrapper.vm as any).handleImageError();
|
|
||||||
}
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).loadAttempts).toBe(3);
|
|
||||||
expect((wrapper.vm as any).canRetry).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets error state when image URL changes", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("complex");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Trigger error first
|
|
||||||
await (wrapper.vm as any).handleImageError();
|
|
||||||
expect((wrapper.vm as any).imageError).toBe(true);
|
|
||||||
|
|
||||||
// Change image URL
|
|
||||||
await wrapper.setProps({ imageUrl: "https://example.com/new-image.jpg" });
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).imageError).toBe(false);
|
|
||||||
expect((wrapper.vm as any).imageLoaded).toBe(false);
|
|
||||||
expect((wrapper.vm as any).loadAttempts).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Accessibility & User Experience", () => {
|
|
||||||
it("has proper ARIA labels", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
const image = wrapper.find('[data-testid="viewer-image"]');
|
|
||||||
expect(image.attributes("alt")).toBe("expanded shared content");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has proper button labels", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
const closeButton = wrapper.find('[data-testid="close-button"]');
|
|
||||||
const shareButton = wrapper.find('[data-testid="share-button"]');
|
|
||||||
|
|
||||||
expect(closeButton.exists()).toBe(true);
|
|
||||||
if ((wrapper.vm as any).isMobile) {
|
|
||||||
expect(shareButton.exists()).toBe(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables buttons during operations", async () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("complex");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Use direct method call instead of trigger
|
|
||||||
await (wrapper.vm as any).handleShare();
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).isSharing).toBe(false); // Should be false after completion
|
|
||||||
});
|
|
||||||
|
|
||||||
it("provides visual feedback during operations", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("complex");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).isClosing).toBe(false);
|
|
||||||
expect((wrapper.vm as any).isSharing).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Performance & Transitions", () => {
|
|
||||||
it("uses Vue transitions", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Check that the component renders properly
|
|
||||||
expect(wrapper.find('[data-testid="close-button"]').exists()).toBe(true);
|
|
||||||
expect(wrapper.find('[data-testid="viewer-image"]').exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses Teleport for modal rendering", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
// Check that the component renders properly without Teleport complexity
|
|
||||||
expect(wrapper.find('[data-testid="close-button"]').exists()).toBe(true);
|
|
||||||
expect(wrapper.find('[data-testid="viewer-image"]').exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tracks analytics for performance monitoring", () => {
|
|
||||||
const createWrapper = createImageViewerMockWrapper("integration");
|
|
||||||
wrapper = createWrapper(createMockImageData({ isOpen: true }));
|
|
||||||
|
|
||||||
const analytics = (wrapper.vm as any).getAnalytics();
|
|
||||||
expect(analytics.openCount).toBe(1);
|
|
||||||
expect(analytics.closeCount).toBe(0);
|
|
||||||
expect(analytics.shareCount).toBe(0);
|
|
||||||
expect(analytics.errorCount).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test Scenarios", () => {
|
|
||||||
it("runs through all test scenarios", () => {
|
|
||||||
const scenarios = createImageViewerTestScenarios();
|
|
||||||
|
|
||||||
expect(scenarios.basic).toBeDefined();
|
|
||||||
expect(scenarios.mobile).toBeDefined();
|
|
||||||
expect(scenarios.desktop).toBeDefined();
|
|
||||||
expect(scenarios.imageLoading).toBeDefined();
|
|
||||||
expect(scenarios.imageError).toBeDefined();
|
|
||||||
expect(scenarios.shareSuccess).toBeDefined();
|
|
||||||
expect(scenarios.shareFallback).toBeDefined();
|
|
||||||
expect(scenarios.shareError).toBeDefined();
|
|
||||||
expect(scenarios.accessibility).toBeDefined();
|
|
||||||
expect(scenarios.performance).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("validates basic scenario behavior", () => {
|
|
||||||
const scenarios = createImageViewerTestScenarios();
|
|
||||||
const createWrapper = createImageViewerMockWrapper("simple");
|
|
||||||
|
|
||||||
wrapper = createWrapper(scenarios.basic.props);
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
expect(scenarios.basic.expectedBehavior).toBe("Component renders with basic props");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("validates mobile scenario behavior", () => {
|
|
||||||
const scenarios = createImageViewerTestScenarios();
|
|
||||||
const createWrapper = createImageViewerMockWrapper("standard");
|
|
||||||
|
|
||||||
wrapper = createWrapper(scenarios.mobile.props);
|
|
||||||
(wrapper.vm as any).userAgent = scenarios.mobile.userAgent;
|
|
||||||
|
|
||||||
expect((wrapper.vm as any).isMobile).toBe(true);
|
|
||||||
expect(scenarios.mobile.expectedBehavior).toBe("Share button visible on mobile");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Mock Levels Comparison", () => {
|
|
||||||
it("simple mock provides basic functionality", () => {
|
|
||||||
const simpleMock = createSimpleImageViewerMock();
|
|
||||||
expect(simpleMock.template).toContain("image-viewer-mock");
|
|
||||||
expect(simpleMock.emits).toEqual(["update:isOpen"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("standard mock provides realistic behavior", () => {
|
|
||||||
const standardMock = createStandardImageViewerMock();
|
|
||||||
expect(standardMock.template).toContain("data-testid");
|
|
||||||
expect(standardMock.template).toContain("close-button");
|
|
||||||
expect(standardMock.computed).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("complex mock provides error handling", () => {
|
|
||||||
const complexMock = createComplexImageViewerMock();
|
|
||||||
expect(complexMock.template).toContain("imageError");
|
|
||||||
expect(complexMock.template).toContain("retryImage");
|
|
||||||
expect(complexMock.emits).toContain("image-error");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("integration mock provides analytics", () => {
|
|
||||||
const integrationMock = createIntegrationImageViewerMock();
|
|
||||||
expect(integrationMock.template).toContain("analytics");
|
|
||||||
expect(integrationMock.methods.getAnalytics).toBeDefined();
|
|
||||||
expect(integrationMock.emits).toContain("share-success");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,180 +0,0 @@
|
|||||||
# TimeSafari Testing Coverage Tracking
|
|
||||||
|
|
||||||
**Project**: TimeSafari
|
|
||||||
**Last Updated**: 2025-08-21T09:40Z
|
|
||||||
**Status**: Active Testing Implementation
|
|
||||||
|
|
||||||
## Current Coverage Status
|
|
||||||
|
|
||||||
### **Simple Components** (6/6 at 100% coverage) ✅
|
|
||||||
|
|
||||||
| Component | Lines | Tests | Coverage | Status | Completed Date |
|
|
||||||
|-----------|-------|-------|----------|---------|----------------|
|
|
||||||
| **RegistrationNotice.vue** | 34 | 34 | 100% | ✅ Complete | 2025-07-29 |
|
|
||||||
| **LargeIdenticonModal.vue** | 39 | 31 | 100% | ✅ Complete | 2025-07-29 |
|
|
||||||
| **ProjectIcon.vue** | 48 | 39 | 100% | ✅ Complete | 2025-07-29 |
|
|
||||||
| **ContactBulkActions.vue** | 43 | 43 | 100% | ✅ Complete | 2025-07-29 |
|
|
||||||
| **EntityIcon.vue** | 82 | 0* | 100% | ✅ Complete | 2025-07-29 |
|
|
||||||
| **ShowAllCard.vue** | 66 | 52 | 100% | ✅ Complete | 2025-08-21 |
|
|
||||||
|
|
||||||
*EntityIcon.vue has 100% coverage but no dedicated test file (covered by
|
|
||||||
LargeIdenticonModal tests)
|
|
||||||
|
|
||||||
### **Medium Components** (0/0 ready for expansion)
|
|
||||||
|
|
||||||
| Component | Lines | Estimated Tests | Priority | Status |
|
|
||||||
|-----------|-------|-----------------|----------|---------|
|
|
||||||
| *Ready for testing implementation* | - | - | - | 🔄 Pending |
|
|
||||||
|
|
||||||
### **Complex Components** (0/0 ready for expansion)
|
|
||||||
|
|
||||||
| Component | Lines | Estimated Tests | Priority | Status |
|
|
||||||
|-----------|-------|-----------------|----------|---------|
|
|
||||||
| *Ready for testing implementation* | - | - | 🔄 Pending |
|
|
||||||
|
|
||||||
## Test Infrastructure Status
|
|
||||||
|
|
||||||
- **Total Tests**: 201 tests passing
|
|
||||||
- **Test Files**: 6 files
|
|
||||||
- **Mock Files**: 7 mock implementations
|
|
||||||
- **Test Categories**: 10 comprehensive categories
|
|
||||||
- **Overall Coverage**: 3.24% (focused on simple components)
|
|
||||||
- **Enhanced Testing**: All simple components now have comprehensive test coverage
|
|
||||||
|
|
||||||
## Implementation Progress
|
|
||||||
|
|
||||||
### **Phase 1: Simple Components** ✅ **COMPLETE**
|
|
||||||
|
|
||||||
**Objective**: Establish 100% coverage for all simple components (<100 lines)
|
|
||||||
|
|
||||||
**Status**: 100% Complete (6/6 components)
|
|
||||||
|
|
||||||
**Components Completed**:
|
|
||||||
- RegistrationNotice.vue (34 lines, 34 tests)
|
|
||||||
- LargeIdenticonModal.vue (39 lines, 31 tests)
|
|
||||||
- ProjectIcon.vue (48 lines, 39 tests)
|
|
||||||
- ContactBulkActions.vue (43 lines, 43 tests)
|
|
||||||
- EntityIcon.vue (82 lines, 0 tests - covered by LargeIdenticonModal)
|
|
||||||
- ShowAllCard.vue (66 lines, 52 tests)
|
|
||||||
|
|
||||||
**Key Achievements**:
|
|
||||||
- Established three-tier mock architecture (Simple/Standard/Complex)
|
|
||||||
- Implemented comprehensive test patterns across 10 categories
|
|
||||||
- Achieved 100% coverage for all simple components
|
|
||||||
- Created reusable mock utilities and testing patterns
|
|
||||||
|
|
||||||
### **Phase 2: Medium Components** 🔄 **READY TO START**
|
|
||||||
|
|
||||||
**Objective**: Expand testing to medium complexity components (100-300 lines)
|
|
||||||
|
|
||||||
**Status**: Ready to begin
|
|
||||||
|
|
||||||
**Target Components**:
|
|
||||||
- Components with 100-300 lines
|
|
||||||
- Focus on business logic components
|
|
||||||
- Priority: High-value, frequently used components
|
|
||||||
|
|
||||||
**Coverage Goals**:
|
|
||||||
- Line Coverage: 95%
|
|
||||||
- Branch Coverage: 90%
|
|
||||||
- Function Coverage: 100%
|
|
||||||
|
|
||||||
### **Phase 3: Complex Components** 🔄 **PLANNED**
|
|
||||||
|
|
||||||
**Objective**: Implement testing for complex components (300+ lines)
|
|
||||||
|
|
||||||
**Status**: Planned for future
|
|
||||||
|
|
||||||
**Target Components**:
|
|
||||||
- Components with 300+ lines
|
|
||||||
- Complex business logic components
|
|
||||||
- Integration-heavy components
|
|
||||||
|
|
||||||
**Coverage Goals**:
|
|
||||||
- Line Coverage: 90%
|
|
||||||
- Branch Coverage: 85%
|
|
||||||
- Function Coverage: 100%
|
|
||||||
|
|
||||||
## Testing Patterns Established
|
|
||||||
|
|
||||||
### **Mock Architecture** ✅
|
|
||||||
|
|
||||||
- **Three-tier system**: Simple/Standard/Complex mocks
|
|
||||||
- **Factory functions**: Specialized mock creation
|
|
||||||
- **Interface compliance**: Full compatibility with original components
|
|
||||||
- **Helper methods**: Common test scenario support
|
|
||||||
|
|
||||||
### **Test Categories** ✅
|
|
||||||
|
|
||||||
1. **Component Rendering** - Structure and conditional rendering
|
|
||||||
2. **Component Styling** - CSS classes and responsive design
|
|
||||||
3. **Component Props** - Validation and handling
|
|
||||||
4. **User Interactions** - Events and accessibility
|
|
||||||
5. **Component Methods** - Functionality and return values
|
|
||||||
6. **Edge Cases** - Null/undefined and rapid changes
|
|
||||||
7. **Error Handling** - Invalid props and graceful degradation
|
|
||||||
8. **Accessibility** - Semantic HTML and ARIA
|
|
||||||
9. **Performance** - Render time and memory leaks
|
|
||||||
10. **Integration** - Parent-child and dependency injection
|
|
||||||
|
|
||||||
### **Advanced Testing Features** ✅
|
|
||||||
|
|
||||||
- **Performance Testing**: Memory leak detection, render time benchmarking
|
|
||||||
- **Snapshot Testing**: DOM structure validation and regression prevention
|
|
||||||
- **Mock Integration**: Mock component validation and testing
|
|
||||||
- **Edge Case Coverage**: Comprehensive error scenario testing
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
### **Immediate Priorities**
|
|
||||||
|
|
||||||
1. **Identify medium complexity components** for Phase 2
|
|
||||||
2. **Prioritize components** by business value and usage frequency
|
|
||||||
3. **Apply established patterns** to medium components
|
|
||||||
4. **Expand mock architecture** for medium complexity needs
|
|
||||||
|
|
||||||
### **Medium Term Goals**
|
|
||||||
|
|
||||||
1. **Achieve 90%+ coverage** for medium components
|
|
||||||
2. **Establish testing patterns** for complex components
|
|
||||||
3. **Implement service layer testing**
|
|
||||||
4. **Add API integration testing**
|
|
||||||
|
|
||||||
### **Long Term Vision**
|
|
||||||
|
|
||||||
1. **Comprehensive test coverage** across all component types
|
|
||||||
2. **Automated testing pipeline** integration
|
|
||||||
3. **Performance regression testing**
|
|
||||||
4. **Cross-browser compatibility testing**
|
|
||||||
|
|
||||||
## Lessons Learned
|
|
||||||
|
|
||||||
### **Success Factors**
|
|
||||||
|
|
||||||
1. **Three-tier mock architecture** provides flexibility and scalability
|
|
||||||
2. **Comprehensive test categories** ensure thorough coverage
|
|
||||||
3. **Performance testing** catches real-world issues early
|
|
||||||
4. **Snapshot testing** prevents regression issues
|
|
||||||
5. **Mock integration testing** validates testing infrastructure
|
|
||||||
|
|
||||||
### **Best Practices Established**
|
|
||||||
|
|
||||||
1. **Start with simple components** to establish patterns
|
|
||||||
2. **Use factory functions** for specialized mock creation
|
|
||||||
3. **Test mocks themselves** to ensure reliability
|
|
||||||
4. **Include performance testing** for stability
|
|
||||||
5. **Document patterns** for team adoption
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- **MDC Guide**: `.cursor/rules/unit_testing_mocks.mdc`
|
|
||||||
- **Test Directory**: `src/test/`
|
|
||||||
- **Mock Implementations**: `src/test/__mocks__/`
|
|
||||||
- **Test Utilities**: `src/test/utils/`
|
|
||||||
- **Examples**: `src/test/examples/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Maintainer**: Development Team
|
|
||||||
**Review Schedule**: Monthly
|
|
||||||
**Next Review**: 2025-09-21
|
|
||||||
@@ -1,624 +0,0 @@
|
|||||||
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("<img");
|
|
||||||
expect(generatedIcon).toContain('src="test-image.jpg"');
|
|
||||||
expect(generatedIcon).toContain('class="w-full h-full object-contain"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should generate SVG HTML when no imageUrl is provided", () => {
|
|
||||||
wrapper = mountComponent({ imageUrl: "", iconSize: 64 });
|
|
||||||
const generatedIcon = wrapper.vm.generateIcon();
|
|
||||||
|
|
||||||
expect(generatedIcon).toContain("<svg");
|
|
||||||
expect(generatedIcon).toContain('width="64"');
|
|
||||||
expect(generatedIcon).toContain('height="64"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should use blank config when no entityId", () => {
|
|
||||||
wrapper = mountComponent({ entityId: "", iconSize: 64 });
|
|
||||||
const generatedIcon = wrapper.vm.generateIcon();
|
|
||||||
|
|
||||||
expect(generatedIcon).toContain("<svg");
|
|
||||||
expect(generatedIcon).toContain('width="64"');
|
|
||||||
expect(generatedIcon).toContain('height="64"');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Component Methods", () => {
|
|
||||||
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(
|
|
||||||
'<img src="test-image.jpg" class="w-full h-full object-contain" />',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should generate correct HTML for SVG", () => {
|
|
||||||
wrapper = mountComponent({ imageUrl: "", iconSize: 32 });
|
|
||||||
const result = wrapper.vm.generateIcon();
|
|
||||||
|
|
||||||
expect(result).toContain("<svg");
|
|
||||||
expect(result).toContain('width="32"');
|
|
||||||
expect(result).toContain('height="32"');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Edge Cases", () => {
|
|
||||||
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 meet WCAG accessibility standards", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const container = wrapper.find(".h-full");
|
|
||||||
|
|
||||||
// Semantic structure
|
|
||||||
expect(container.exists()).toBe(true);
|
|
||||||
expect(container.element.tagName.toLowerCase()).toBe("div");
|
|
||||||
|
|
||||||
// Note: Component lacks ARIA attributes - these should be added for full accessibility
|
|
||||||
// Missing: alt text for images, aria-label for links, focus management
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should support keyboard navigation for links", () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
imageUrl: "test-image.jpg",
|
|
||||||
linkToFullImage: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const link = wrapper.find("a");
|
|
||||||
expect(link.exists()).toBe(true);
|
|
||||||
|
|
||||||
// Test keyboard interaction
|
|
||||||
link.trigger("keydown.enter");
|
|
||||||
// Note: Link behavior would be tested in integration tests
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper image accessibility", () => {
|
|
||||||
wrapper = mountComponent({ imageUrl: "test-image.jpg" });
|
|
||||||
const html = wrapper.html();
|
|
||||||
|
|
||||||
// Verify image has proper attributes
|
|
||||||
expect(html).toContain("<img");
|
|
||||||
expect(html).toContain('src="test-image.jpg"');
|
|
||||||
expect(html).toContain('class="w-full h-full object-contain"');
|
|
||||||
|
|
||||||
// Note: Missing alt text - should be added for accessibility
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have proper SVG accessibility", () => {
|
|
||||||
wrapper = mountComponent({ imageUrl: "", iconSize: 64 });
|
|
||||||
const html = wrapper.html();
|
|
||||||
|
|
||||||
// Verify SVG has proper attributes
|
|
||||||
expect(html).toContain("<svg");
|
|
||||||
expect(html).toContain('xmlns="http://www.w3.org/2000/svg"');
|
|
||||||
|
|
||||||
// Note: Missing aria-label or title - should be added for accessibility
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should maintain accessibility 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 container = testWrapper.find(".h-full");
|
|
||||||
|
|
||||||
// Core accessibility structure should always be present
|
|
||||||
expect(container.exists()).toBe(true);
|
|
||||||
|
|
||||||
if (props.imageUrl && props.linkToFullImage) {
|
|
||||||
// Link should be accessible
|
|
||||||
const link = testWrapper.find("a");
|
|
||||||
expect(link.exists()).toBe(true);
|
|
||||||
expect(link.attributes("target")).toBe("_blank");
|
|
||||||
expect(link.element.tagName.toLowerCase()).toBe("a");
|
|
||||||
} else {
|
|
||||||
// Div should be accessible
|
|
||||||
expect(container.element.tagName.toLowerCase()).toBe("div");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have sufficient color contrast", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const container = wrapper.find(".h-full");
|
|
||||||
|
|
||||||
// Verify container has proper styling
|
|
||||||
expect(container.classes()).toContain("h-full");
|
|
||||||
expect(container.classes()).toContain("w-full");
|
|
||||||
expect(container.classes()).toContain("object-contain");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have descriptive content", () => {
|
|
||||||
wrapper = mountComponent({ entityId: "test-entity" });
|
|
||||||
|
|
||||||
// Component should render content based on entityId
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
expect(wrapper.find(".h-full").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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Error Handling", () => {
|
|
||||||
it("should handle null entityId gracefully", () => {
|
|
||||||
wrapper = mountComponent({ entityId: null as any });
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle undefined imageUrl gracefully", () => {
|
|
||||||
wrapper = mountComponent({ imageUrl: undefined as any });
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle malformed props without crashing", () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
entityId: "invalid",
|
|
||||||
iconSize: "invalid" as any,
|
|
||||||
imageUrl: "invalid",
|
|
||||||
linkToFullImage: "invalid" as any,
|
|
||||||
});
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle rapid prop changes without errors", async () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
|
|
||||||
// Rapidly change props
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
await wrapper.setProps({
|
|
||||||
entityId: `entity-${i}`,
|
|
||||||
iconSize: i * 10,
|
|
||||||
imageUrl: i % 2 === 0 ? `image-${i}.jpg` : "",
|
|
||||||
linkToFullImage: i % 2 === 0,
|
|
||||||
});
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Performance Testing", () => {
|
|
||||||
it("should render within acceptable time", () => {
|
|
||||||
const start = performance.now();
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const end = performance.now();
|
|
||||||
|
|
||||||
expect(end - start).toBeLessThan(50); // 50ms threshold
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle rapid prop changes efficiently", async () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const start = performance.now();
|
|
||||||
|
|
||||||
// Rapidly change props
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
await wrapper.setProps({
|
|
||||||
entityId: `entity-${i}`,
|
|
||||||
iconSize: (i % 50) + 10,
|
|
||||||
});
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
}
|
|
||||||
|
|
||||||
const end = performance.now();
|
|
||||||
expect(end - start).toBeLessThan(1000); // 1 second threshold
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not cause memory leaks with icon generation", async () => {
|
|
||||||
// Create and destroy multiple components
|
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
const tempWrapper = mountComponent({ entityId: `entity-${i}` });
|
|
||||||
tempWrapper.unmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force garbage collection if available
|
|
||||||
if (global.gc) {
|
|
||||||
global.gc();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify component cleanup
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Integration Testing", () => {
|
|
||||||
it("should work with parent component context", () => {
|
|
||||||
// Mock parent component
|
|
||||||
const ParentComponent = {
|
|
||||||
template: `
|
|
||||||
<div>
|
|
||||||
<ProjectIcon
|
|
||||||
:entityId="entityId"
|
|
||||||
:iconSize="iconSize"
|
|
||||||
:imageUrl="imageUrl"
|
|
||||||
:linkToFullImage="linkToFullImage"
|
|
||||||
@click="handleClick"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
components: { ProjectIcon },
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
entityId: "test-entity",
|
|
||||||
iconSize: 64,
|
|
||||||
imageUrl: "",
|
|
||||||
linkToFullImage: false,
|
|
||||||
clickCalled: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleClick() {
|
|
||||||
(this as any).clickCalled = true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const parentWrapper = mount(ParentComponent);
|
|
||||||
const icon = parentWrapper.findComponent(ProjectIcon);
|
|
||||||
|
|
||||||
expect(icon.exists()).toBe(true);
|
|
||||||
expect((parentWrapper.vm as any).clickCalled).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should integrate with image service", () => {
|
|
||||||
// Mock image service
|
|
||||||
const imageService = {
|
|
||||||
getImageUrl: vi.fn().mockReturnValue("https://example.com/image.jpg"),
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper = mountComponent({
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
imageService,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
expect(imageService.getImageUrl).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with global properties", () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
global: {
|
|
||||||
config: {
|
|
||||||
globalProperties: {
|
|
||||||
$t: (key: string) => key,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Snapshot Testing", () => {
|
|
||||||
it("should maintain consistent DOM structure", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const html = wrapper.html();
|
|
||||||
|
|
||||||
// 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"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have consistent CSS classes", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const container = wrapper.find(".h-full");
|
|
||||||
const image = wrapper.find(".w-full");
|
|
||||||
|
|
||||||
// Verify container classes
|
|
||||||
const expectedContainerClasses = ["h-full", "w-full", "object-contain"];
|
|
||||||
|
|
||||||
expectedContainerClasses.forEach((className) => {
|
|
||||||
expect(container.classes()).toContain(className);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify image classes
|
|
||||||
const expectedImageClasses = ["w-full", "h-full", "object-contain"];
|
|
||||||
|
|
||||||
expectedImageClasses.forEach((className) => {
|
|
||||||
expect(image.classes()).toContain(className);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should maintain accessibility structure", () => {
|
|
||||||
wrapper = mountComponent();
|
|
||||||
const container = wrapper.find(".h-full");
|
|
||||||
const image = wrapper.find(".w-full");
|
|
||||||
|
|
||||||
// Verify basic structure
|
|
||||||
expect(container.exists()).toBe(true);
|
|
||||||
expect(image.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,655 +0,0 @@
|
|||||||
# TimeSafari Unit Testing Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This directory contains comprehensive unit tests for TimeSafari components using
|
|
||||||
**Vitest** and **JSDOM**. The testing infrastructure is designed to work with
|
|
||||||
Vue 3 components using the `vue-facing-decorator` pattern.
|
|
||||||
|
|
||||||
## Current Coverage Status
|
|
||||||
|
|
||||||
### ✅ **100% Coverage Components** (6 components)
|
|
||||||
|
|
||||||
| Component | Lines | Tests | Coverage |
|
|
||||||
|-----------|-------|-------|----------|
|
|
||||||
| **RegistrationNotice.vue** | 34 | 34 | 100% |
|
|
||||||
| **LargeIdenticonModal.vue** | 39 | 31 | 100% |
|
|
||||||
| **ProjectIcon.vue** | 48 | 39 | 100% |
|
|
||||||
| **ContactBulkActions.vue** | 43 | 43 | 100% |
|
|
||||||
| **EntityIcon.vue** | 82 | 0* | 100% |
|
|
||||||
| **ShowAllCard.vue** | 66 | 52 | 100% |
|
|
||||||
|
|
||||||
*EntityIcon.vue has 100% coverage but no dedicated test file (covered by
|
|
||||||
LargeIdenticonModal tests)
|
|
||||||
|
|
||||||
### 📊 **Coverage Metrics**
|
|
||||||
|
|
||||||
- **Total Tests**: 201 tests passing
|
|
||||||
- **Test Files**: 6 files
|
|
||||||
- **Components Covered**: 6 simple components
|
|
||||||
- **Mock Files**: 7 mock implementations
|
|
||||||
- **Overall Coverage**: 3.24% (focused on simple components)
|
|
||||||
- **Test Categories**: 10 comprehensive categories
|
|
||||||
- **Enhanced Testing**: All simple components now have comprehensive test coverage
|
|
||||||
|
|
||||||
> **📋 Project Tracking**: For detailed coverage metrics, implementation progress, and
|
|
||||||
> project-specific status, see [`PROJECT_COVERAGE_TRACKING.md`](./PROJECT_COVERAGE_TRACKING.md)
|
|
||||||
|
|
||||||
## Testing Infrastructure
|
|
||||||
|
|
||||||
### **Core Technologies**
|
|
||||||
|
|
||||||
- **Vitest**: Fast unit testing framework
|
|
||||||
- **JSDOM**: Browser-like environment for Node.js
|
|
||||||
- **@vue/test-utils**: Vue component testing utilities
|
|
||||||
- **TypeScript**: Full type safety for tests
|
|
||||||
|
|
||||||
### **Configuration Files**
|
|
||||||
|
|
||||||
- `vitest.config.ts` - Vitest configuration with JSDOM environment
|
|
||||||
- `src/test/setup.ts` - Global test setup and mocks
|
|
||||||
- `package.json` - Test scripts and dependencies
|
|
||||||
|
|
||||||
### **Global Mocks**
|
|
||||||
|
|
||||||
The test environment includes comprehensive mocks for browser APIs:
|
|
||||||
|
|
||||||
- `ResizeObserver` - For responsive component testing
|
|
||||||
- `IntersectionObserver` - For scroll-based components
|
|
||||||
- `localStorage` / `sessionStorage` - For data persistence
|
|
||||||
- `matchMedia` - For responsive design testing
|
|
||||||
- `console` methods - For clean test output
|
|
||||||
|
|
||||||
## Test Patterns
|
|
||||||
|
|
||||||
### **1. Component Mounting**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const mountComponent = (props = {}) => {
|
|
||||||
return mount(ComponentName, {
|
|
||||||
props: {
|
|
||||||
// Default props
|
|
||||||
...props
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### **2. Event Testing**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it('should emit event when clicked', async () => {
|
|
||||||
wrapper = mountComponent()
|
|
||||||
await wrapper.find('button').trigger('click')
|
|
||||||
expect(wrapper.emitted('event-name')).toBeTruthy()
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **3. Prop Validation**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it('should accept all required props', () => {
|
|
||||||
wrapper = mountComponent()
|
|
||||||
expect(wrapper.vm.propName).toBeDefined()
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **4. CSS Class Testing**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it('should have correct CSS classes', () => {
|
|
||||||
wrapper = mountComponent()
|
|
||||||
const element = wrapper.find('.selector')
|
|
||||||
expect(element.classes()).toContain('expected-class')
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Categories
|
|
||||||
|
|
||||||
### **Component Rendering**
|
|
||||||
|
|
||||||
- Component existence and structure
|
|
||||||
- Conditional rendering based on props
|
|
||||||
- Template structure validation
|
|
||||||
|
|
||||||
### **Component Styling**
|
|
||||||
|
|
||||||
- CSS class application
|
|
||||||
- Responsive design classes
|
|
||||||
- Tailwind CSS integration
|
|
||||||
|
|
||||||
### **Component Props**
|
|
||||||
|
|
||||||
- Required prop validation
|
|
||||||
- Optional prop handling
|
|
||||||
- Prop type checking
|
|
||||||
|
|
||||||
### **User Interactions**
|
|
||||||
|
|
||||||
- Click event handling
|
|
||||||
- Form input interactions
|
|
||||||
- Keyboard navigation
|
|
||||||
|
|
||||||
### **Component Methods**
|
|
||||||
|
|
||||||
- Method existence and functionality
|
|
||||||
- Return value validation
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### **Edge Cases**
|
|
||||||
|
|
||||||
- Empty/null prop handling
|
|
||||||
- Rapid user interactions
|
|
||||||
- Component state changes
|
|
||||||
|
|
||||||
### **Accessibility**
|
|
||||||
|
|
||||||
- Semantic HTML structure
|
|
||||||
- ARIA attributes
|
|
||||||
- Keyboard navigation
|
|
||||||
|
|
||||||
### **Error Handling** ✅ **NEW**
|
|
||||||
|
|
||||||
- Invalid prop combinations
|
|
||||||
- Malformed data handling
|
|
||||||
- Graceful degradation
|
|
||||||
- Exception handling
|
|
||||||
|
|
||||||
### **Performance Testing** ✅ **NEW**
|
|
||||||
|
|
||||||
- Render time benchmarks
|
|
||||||
- Memory leak detection
|
|
||||||
- Rapid re-render efficiency
|
|
||||||
- Component cleanup validation
|
|
||||||
|
|
||||||
### **Integration Testing** ✅ **NEW**
|
|
||||||
|
|
||||||
- Parent-child component interaction
|
|
||||||
- Dependency injection testing
|
|
||||||
- Global property integration
|
|
||||||
- Service integration patterns
|
|
||||||
|
|
||||||
### **Snapshot Testing** ✅ **NEW**
|
|
||||||
|
|
||||||
- DOM structure validation
|
|
||||||
- CSS class regression detection
|
|
||||||
- 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**
|
|
||||||
|
|
||||||
Each mock component provides:
|
|
||||||
|
|
||||||
- Same interface as original component
|
|
||||||
- Simplified behavior for testing
|
|
||||||
- Helper methods for test scenarios
|
|
||||||
- Computed properties for state validation
|
|
||||||
|
|
||||||
### **Mock Usage Examples**
|
|
||||||
|
|
||||||
#### **Direct Instantiation**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import RegistrationNoticeMock from '@/test/__mocks__/RegistrationNotice.mock'
|
|
||||||
const mock = new RegistrationNoticeMock()
|
|
||||||
expect(mock.shouldShow).toBe(true)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Vue Test Utils Integration**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import RegistrationNoticeMock from '@/test/__mocks__/RegistrationNotice.mock'
|
|
||||||
|
|
||||||
const wrapper = mount(RegistrationNoticeMock, {
|
|
||||||
props: { isRegistered: false, show: true }
|
|
||||||
})
|
|
||||||
expect(wrapper.vm.shouldShow).toBe(true)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Event Testing**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const mock = new RegistrationNoticeMock()
|
|
||||||
mock.mockShareInfoClick()
|
|
||||||
// Verify event was emitted
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Custom Mock Behavior**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class CustomRegistrationNoticeMock extends RegistrationNoticeMock {
|
|
||||||
get shouldShow(): boolean {
|
|
||||||
return false // Override for specific test scenario
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced Testing Patterns
|
|
||||||
|
|
||||||
### **Spy Methods**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { vi } from 'vitest'
|
|
||||||
|
|
||||||
it('should call method when triggered', () => {
|
|
||||||
const mockMethod = vi.fn()
|
|
||||||
wrapper = mountComponent()
|
|
||||||
wrapper.vm.someMethod = mockMethod
|
|
||||||
|
|
||||||
wrapper.vm.triggerMethod()
|
|
||||||
expect(mockMethod).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Integration Testing**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it('should work with parent component', () => {
|
|
||||||
const parentWrapper = mount(ParentComponent, {
|
|
||||||
global: {
|
|
||||||
stubs: {
|
|
||||||
ChildComponent: RegistrationNoticeMock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(parentWrapper.findComponent(RegistrationNoticeMock).exists()).toBe(true)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **State Change Testing**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it('should update state when props change', async () => {
|
|
||||||
wrapper = mountComponent({ show: false })
|
|
||||||
expect(wrapper.find('.notice').exists()).toBe(false)
|
|
||||||
|
|
||||||
await wrapper.setProps({ show: true })
|
|
||||||
expect(wrapper.find('.notice').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Performance Testing**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it('should render within acceptable time', () => {
|
|
||||||
const start = performance.now()
|
|
||||||
wrapper = mountComponent()
|
|
||||||
const end = performance.now()
|
|
||||||
|
|
||||||
expect(end - start).toBeLessThan(100) // 100ms threshold
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
### **Available Commands**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
npm run test:unit
|
|
||||||
|
|
||||||
# Run tests in watch mode
|
|
||||||
npm run test:unit:watch
|
|
||||||
|
|
||||||
# Run tests with coverage
|
|
||||||
npm run test:unit:coverage
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
npm run test:unit src/test/RegistrationNotice.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Test Output**
|
|
||||||
|
|
||||||
- **Passing Tests**: Green checkmarks
|
|
||||||
- **Failing Tests**: Red X with detailed error messages
|
|
||||||
- **Coverage Report**: Percentage coverage for each file
|
|
||||||
- **Performance Metrics**: Test execution times
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/test/
|
|
||||||
├── __mocks__/ # Mock component implementations
|
|
||||||
│ ├── RegistrationNotice.mock.ts
|
|
||||||
│ ├── LargeIdenticonModal.mock.ts
|
|
||||||
│ ├── ProjectIcon.mock.ts
|
|
||||||
│ ├── ContactBulkActions.mock.ts
|
|
||||||
│ ├── ImageViewer.mock.ts
|
|
||||||
│ ├── ShowAllCard.mock.ts # Mock with Simple/Standard/Complex levels
|
|
||||||
│ └── README.md # Mock usage documentation
|
|
||||||
├── 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
|
|
||||||
├── LargeIdenticonModal.test.ts # Component tests
|
|
||||||
├── ProjectIcon.test.ts # Component tests
|
|
||||||
├── ContactBulkActions.test.ts # Component tests
|
|
||||||
├── ShowAllCard.test.ts # Component tests (52 tests, 100% coverage)
|
|
||||||
└── 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**
|
|
||||||
|
|
||||||
1. **Group related tests** using `describe` blocks
|
|
||||||
2. **Use descriptive test names** that explain the scenario
|
|
||||||
3. **Keep tests focused** on one specific behavior
|
|
||||||
4. **Use helper functions** for common setup
|
|
||||||
|
|
||||||
### **Mock Design**
|
|
||||||
|
|
||||||
1. **Maintain interface compatibility** with original components
|
|
||||||
2. **Provide helper methods** for common test scenarios
|
|
||||||
3. **Include computed properties** for state validation
|
|
||||||
4. **Document mock behavior** clearly
|
|
||||||
|
|
||||||
### **Coverage Goals**
|
|
||||||
|
|
||||||
1. **100% line coverage** for simple components
|
|
||||||
2. **100% branch coverage** for conditional logic
|
|
||||||
3. **100% function coverage** for all methods
|
|
||||||
4. **Edge case coverage** for error scenarios
|
|
||||||
|
|
||||||
## Future Improvements
|
|
||||||
|
|
||||||
### **Implemented Enhancements**
|
|
||||||
|
|
||||||
1. ✅ **Error handling** - Component error states and exception handling
|
|
||||||
2. ✅ **Performance testing** - Render time benchmarks and memory leak detection
|
|
||||||
3. ✅ **Integration testing** - Parent-child component interaction and dependency injection
|
|
||||||
4. ✅ **Snapshot testing** - DOM structure validation and CSS class regression detection
|
|
||||||
5. ✅ **Accessibility compliance** - ARIA attributes and semantic structure validation
|
|
||||||
|
|
||||||
### **Future Enhancements**
|
|
||||||
|
|
||||||
1. **Visual regression testing** - Automated UI consistency checks
|
|
||||||
2. **Cross-browser compatibility** testing
|
|
||||||
3. **Service layer integration** testing
|
|
||||||
4. **End-to-end component** testing
|
|
||||||
5. **Advanced performance** profiling
|
|
||||||
|
|
||||||
### **Coverage Expansion**
|
|
||||||
|
|
||||||
1. **Medium complexity components** (100-300 lines)
|
|
||||||
2. **Complex components** (300+ lines)
|
|
||||||
3. **Service layer testing**
|
|
||||||
4. **Utility function testing**
|
|
||||||
5. **API integration testing**
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### **Common Issues**
|
|
||||||
|
|
||||||
1. **Import errors**: Check path aliases in `vitest.config.ts`
|
|
||||||
2. **Mock not found**: Verify mock file exists and exports correctly
|
|
||||||
3. **Test failures**: Check for timing issues with async operations
|
|
||||||
4. **Coverage gaps**: Add tests for uncovered code paths
|
|
||||||
|
|
||||||
### **Debug Tips**
|
|
||||||
|
|
||||||
1. **Use `console.log`** in tests for debugging
|
|
||||||
2. **Check test output** for detailed error messages
|
|
||||||
3. **Verify component props** are being passed correctly
|
|
||||||
4. **Test one assertion at a time** to isolate issues
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: July 29, 2025*
|
|
||||||
*Test infrastructure established with 100% coverage for 5 simple components*
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,494 +0,0 @@
|
|||||||
/**
|
|
||||||
* ShowAllCard Component Tests
|
|
||||||
*
|
|
||||||
* Comprehensive unit tests covering all required test categories:
|
|
||||||
* - Component Rendering
|
|
||||||
* - Component Styling
|
|
||||||
* - Component Props
|
|
||||||
* - User Interactions
|
|
||||||
* - Component Methods
|
|
||||||
* - Edge Cases
|
|
||||||
* - Error Handling
|
|
||||||
* - Accessibility
|
|
||||||
* - Performance
|
|
||||||
* - Integration
|
|
||||||
*
|
|
||||||
* @author Matthew Raymer
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { mount, VueWrapper } from '@vue/test-utils'
|
|
||||||
import ShowAllCard from '@/components/ShowAllCard.vue'
|
|
||||||
import {
|
|
||||||
ShowAllCardSimpleMock,
|
|
||||||
ShowAllCardStandardMock,
|
|
||||||
ShowAllCardComplexMock,
|
|
||||||
createPeopleShowAllCardMock,
|
|
||||||
createProjectsShowAllCardMock,
|
|
||||||
createShowAllCardMockWithComplexQuery
|
|
||||||
} from './__mocks__/ShowAllCard.mock'
|
|
||||||
|
|
||||||
describe('ShowAllCard', () => {
|
|
||||||
let wrapper: VueWrapper<any>
|
|
||||||
|
|
||||||
// Default props for testing
|
|
||||||
const defaultProps = {
|
|
||||||
entityType: 'people' as const,
|
|
||||||
routeName: 'contacts',
|
|
||||||
queryParams: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component wrapper factory
|
|
||||||
const mountComponent = (props = {}) => {
|
|
||||||
return mount(ShowAllCard, {
|
|
||||||
props: { ...defaultProps, ...props }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = mountComponent()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
wrapper?.unmount()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Component Rendering', () => {
|
|
||||||
it('should render correctly', () => {
|
|
||||||
expect(wrapper.exists()).toBe(true)
|
|
||||||
expect(wrapper.find('li').exists()).toBe(true)
|
|
||||||
expect(wrapper.find('router-link').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render with correct structure', () => {
|
|
||||||
const listItem = wrapper.find('li')
|
|
||||||
const routerLink = wrapper.find('router-link')
|
|
||||||
const icon = wrapper.find('font-awesome')
|
|
||||||
const title = wrapper.find('h3')
|
|
||||||
|
|
||||||
expect(listItem.exists()).toBe(true)
|
|
||||||
expect(routerLink.exists()).toBe(true)
|
|
||||||
expect(icon.exists()).toBe(true)
|
|
||||||
expect(title.exists()).toBe(true)
|
|
||||||
expect(title.text()).toBe('Show All')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render conditionally based on props', () => {
|
|
||||||
wrapper = mountComponent({ entityType: 'projects' })
|
|
||||||
expect(wrapper.exists()).toBe(true)
|
|
||||||
|
|
||||||
wrapper = mountComponent({ entityType: 'people' })
|
|
||||||
expect(wrapper.exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should render with different entity types', () => {
|
|
||||||
const peopleWrapper = mountComponent({ entityType: 'people' })
|
|
||||||
const projectsWrapper = mountComponent({ entityType: 'projects' })
|
|
||||||
|
|
||||||
expect(peopleWrapper.exists()).toBe(true)
|
|
||||||
expect(projectsWrapper.exists()).toBe(true)
|
|
||||||
|
|
||||||
peopleWrapper.unmount()
|
|
||||||
projectsWrapper.unmount()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Component Styling', () => {
|
|
||||||
it('should have correct CSS classes on list item', () => {
|
|
||||||
const listItem = wrapper.find('li')
|
|
||||||
expect(listItem.classes()).toContain('cursor-pointer')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have correct CSS classes on icon', () => {
|
|
||||||
const icon = wrapper.find('font-awesome')
|
|
||||||
expect(icon.exists()).toBe(true)
|
|
||||||
expect(icon.attributes('icon')).toBe('circle-right')
|
|
||||||
expect(icon.classes()).toContain('text-blue-500')
|
|
||||||
expect(icon.classes()).toContain('text-5xl')
|
|
||||||
expect(icon.classes()).toContain('mb-1')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have correct CSS classes on title', () => {
|
|
||||||
const title = wrapper.find('h3')
|
|
||||||
expect(title.classes()).toContain('text-xs')
|
|
||||||
expect(title.classes()).toContain('text-slate-500')
|
|
||||||
expect(title.classes()).toContain('font-medium')
|
|
||||||
expect(title.classes()).toContain('italic')
|
|
||||||
expect(title.classes()).toContain('text-ellipsis')
|
|
||||||
expect(title.classes()).toContain('whitespace-nowrap')
|
|
||||||
expect(title.classes()).toContain('overflow-hidden')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have responsive design classes', () => {
|
|
||||||
const title = wrapper.find('h3')
|
|
||||||
expect(title.classes()).toContain('text-ellipsis')
|
|
||||||
expect(title.classes()).toContain('whitespace-nowrap')
|
|
||||||
expect(title.classes()).toContain('overflow-hidden')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have Tailwind CSS integration', () => {
|
|
||||||
const icon = wrapper.find('font-awesome')
|
|
||||||
const title = wrapper.find('h3')
|
|
||||||
|
|
||||||
expect(icon.classes()).toContain('text-blue-500')
|
|
||||||
expect(icon.classes()).toContain('text-5xl')
|
|
||||||
expect(title.classes()).toContain('text-slate-500')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Component Props', () => {
|
|
||||||
it('should accept all required props', () => {
|
|
||||||
expect(wrapper.vm.entityType).toBe('people')
|
|
||||||
expect(wrapper.vm.routeName).toBe('contacts')
|
|
||||||
expect(wrapper.vm.queryParams).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle required entityType prop', () => {
|
|
||||||
wrapper = mountComponent({ entityType: 'projects' })
|
|
||||||
expect(wrapper.vm.entityType).toBe('projects')
|
|
||||||
|
|
||||||
wrapper = mountComponent({ entityType: 'people' })
|
|
||||||
expect(wrapper.vm.entityType).toBe('people')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle required routeName prop', () => {
|
|
||||||
wrapper = mountComponent({ routeName: 'projects' })
|
|
||||||
expect(wrapper.vm.routeName).toBe('projects')
|
|
||||||
|
|
||||||
wrapper = mountComponent({ routeName: 'contacts' })
|
|
||||||
expect(wrapper.vm.routeName).toBe('contacts')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle optional queryParams prop', () => {
|
|
||||||
const queryParams = { filter: 'active', sort: 'name' }
|
|
||||||
wrapper = mountComponent({ queryParams })
|
|
||||||
expect(wrapper.vm.queryParams).toEqual(queryParams)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty queryParams prop', () => {
|
|
||||||
wrapper = mountComponent({ queryParams: {} })
|
|
||||||
expect(wrapper.vm.queryParams).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle undefined queryParams prop', () => {
|
|
||||||
wrapper = mountComponent({ queryParams: undefined })
|
|
||||||
expect(wrapper.vm.queryParams).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should validate prop types correctly', () => {
|
|
||||||
expect(typeof wrapper.vm.entityType).toBe('string')
|
|
||||||
expect(typeof wrapper.vm.routeName).toBe('string')
|
|
||||||
expect(typeof wrapper.vm.queryParams).toBe('object')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('User Interactions', () => {
|
|
||||||
it('should have clickable router link', () => {
|
|
||||||
const routerLink = wrapper.find('router-link')
|
|
||||||
expect(routerLink.exists()).toBe(true)
|
|
||||||
expect(routerLink.attributes('to')).toBeDefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have accessible cursor pointer', () => {
|
|
||||||
const listItem = wrapper.find('li')
|
|
||||||
expect(listItem.classes()).toContain('cursor-pointer')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should support keyboard navigation', () => {
|
|
||||||
const routerLink = wrapper.find('router-link')
|
|
||||||
expect(routerLink.exists()).toBe(true)
|
|
||||||
// Router link should be keyboard accessible by default
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have hover effects defined in CSS', () => {
|
|
||||||
// Check that hover effects are defined in the component's style section
|
|
||||||
const component = wrapper.vm
|
|
||||||
expect(component).toBeDefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Component Methods', () => {
|
|
||||||
it('should have navigationRoute computed property', () => {
|
|
||||||
expect(wrapper.vm.navigationRoute).toBeDefined()
|
|
||||||
expect(typeof wrapper.vm.navigationRoute).toBe('object')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should compute navigationRoute correctly', () => {
|
|
||||||
const expectedRoute = {
|
|
||||||
name: 'contacts',
|
|
||||||
query: {}
|
|
||||||
}
|
|
||||||
expect(wrapper.vm.navigationRoute).toEqual(expectedRoute)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should compute navigationRoute with custom props', () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
routeName: 'projects',
|
|
||||||
queryParams: { filter: 'active' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const expectedRoute = {
|
|
||||||
name: 'projects',
|
|
||||||
query: { filter: 'active' }
|
|
||||||
}
|
|
||||||
expect(wrapper.vm.navigationRoute).toEqual(expectedRoute)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle complex query parameters', () => {
|
|
||||||
const complexQuery = {
|
|
||||||
filter: 'active',
|
|
||||||
sort: 'name',
|
|
||||||
page: '1',
|
|
||||||
limit: '20'
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper = mountComponent({ queryParams: complexQuery })
|
|
||||||
|
|
||||||
const expectedRoute = {
|
|
||||||
name: 'contacts',
|
|
||||||
query: complexQuery
|
|
||||||
}
|
|
||||||
expect(wrapper.vm.navigationRoute).toEqual(expectedRoute)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Edge Cases', () => {
|
|
||||||
it('should handle empty string routeName', () => {
|
|
||||||
wrapper = mountComponent({ routeName: '' })
|
|
||||||
expect(wrapper.vm.navigationRoute).toEqual({
|
|
||||||
name: '',
|
|
||||||
query: {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle null queryParams', () => {
|
|
||||||
wrapper = mountComponent({ queryParams: null as any })
|
|
||||||
expect(wrapper.vm.navigationRoute).toEqual({
|
|
||||||
name: 'contacts',
|
|
||||||
query: null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle undefined queryParams', () => {
|
|
||||||
wrapper = mountComponent({ queryParams: undefined })
|
|
||||||
expect(wrapper.vm.navigationRoute).toEqual({
|
|
||||||
name: 'contacts',
|
|
||||||
query: {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty object queryParams', () => {
|
|
||||||
wrapper = mountComponent({ queryParams: {} })
|
|
||||||
expect(wrapper.vm.navigationRoute).toEqual({
|
|
||||||
name: 'contacts',
|
|
||||||
query: {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle rapid prop changes', async () => {
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
await wrapper.setProps({
|
|
||||||
entityType: i % 2 === 0 ? 'people' : 'projects',
|
|
||||||
routeName: `route-${i}`,
|
|
||||||
queryParams: { index: i.toString() }
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.vm.entityType).toBe(i % 2 === 0 ? 'people' : 'projects')
|
|
||||||
expect(wrapper.vm.routeName).toBe(`route-${i}`)
|
|
||||||
expect(wrapper.vm.queryParams).toEqual({ index: i.toString() })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Error Handling', () => {
|
|
||||||
it('should handle invalid entityType gracefully', () => {
|
|
||||||
wrapper = mountComponent({ entityType: 'invalid' as any })
|
|
||||||
expect(wrapper.exists()).toBe(true)
|
|
||||||
expect(wrapper.vm.entityType).toBe('invalid')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle malformed queryParams gracefully', () => {
|
|
||||||
wrapper = mountComponent({ queryParams: 'invalid' as any })
|
|
||||||
expect(wrapper.exists()).toBe(true)
|
|
||||||
// Should handle gracefully even with invalid queryParams
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle missing props gracefully', () => {
|
|
||||||
// Component should not crash with missing props
|
|
||||||
expect(() => mountComponent({})).not.toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle extreme prop values', () => {
|
|
||||||
const extremeProps = {
|
|
||||||
entityType: 'people',
|
|
||||||
routeName: 'a'.repeat(1000),
|
|
||||||
queryParams: { key: 'value'.repeat(1000) }
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper = mountComponent(extremeProps)
|
|
||||||
expect(wrapper.exists()).toBe(true)
|
|
||||||
expect(wrapper.vm.routeName).toBe(extremeProps.routeName)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
|
||||||
it('should have semantic HTML structure', () => {
|
|
||||||
expect(wrapper.find('li').exists()).toBe(true)
|
|
||||||
expect(wrapper.find('h3').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have proper heading hierarchy', () => {
|
|
||||||
const heading = wrapper.find('h3')
|
|
||||||
expect(heading.exists()).toBe(true)
|
|
||||||
expect(heading.text()).toBe('Show All')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have accessible icon', () => {
|
|
||||||
const icon = wrapper.find('font-awesome')
|
|
||||||
expect(icon.exists()).toBe(true)
|
|
||||||
expect(icon.attributes('icon')).toBe('circle-right')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have proper text content', () => {
|
|
||||||
const title = wrapper.find('h3')
|
|
||||||
expect(title.text()).toBe('Show All')
|
|
||||||
expect(title.text().trim()).toBe('Show All')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Performance', () => {
|
|
||||||
it('should render within acceptable time', () => {
|
|
||||||
const start = performance.now()
|
|
||||||
wrapper = mountComponent()
|
|
||||||
const end = performance.now()
|
|
||||||
|
|
||||||
expect(end - start).toBeLessThan(100) // 100ms threshold
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle rapid re-renders efficiently', async () => {
|
|
||||||
const start = performance.now()
|
|
||||||
|
|
||||||
for (let i = 0; i < 50; i++) {
|
|
||||||
await wrapper.setProps({
|
|
||||||
entityType: i % 2 === 0 ? 'people' : 'projects',
|
|
||||||
queryParams: { index: i.toString() }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const end = performance.now()
|
|
||||||
expect(end - start).toBeLessThan(500) // 500ms threshold for 50 updates
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not cause memory leaks during prop changes', async () => {
|
|
||||||
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0
|
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
await wrapper.setProps({
|
|
||||||
queryParams: { iteration: i.toString() }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalMemory = (performance as any).memory?.usedJSHeapSize || 0
|
|
||||||
const memoryIncrease = finalMemory - initialMemory
|
|
||||||
|
|
||||||
// Memory increase should be reasonable (less than 10MB)
|
|
||||||
expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Integration', () => {
|
|
||||||
it('should work with router-link integration', () => {
|
|
||||||
const routerLink = wrapper.find('router-link')
|
|
||||||
expect(routerLink.exists()).toBe(true)
|
|
||||||
expect(routerLink.attributes('to')).toBeDefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should work with FontAwesome icon integration', () => {
|
|
||||||
const icon = wrapper.find('font-awesome')
|
|
||||||
expect(icon.exists()).toBe(true)
|
|
||||||
expect(icon.attributes('icon')).toBe('circle-right')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should work with Vue Router navigation', () => {
|
|
||||||
const navigationRoute = wrapper.vm.navigationRoute
|
|
||||||
expect(navigationRoute).toHaveProperty('name')
|
|
||||||
expect(navigationRoute).toHaveProperty('query')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should integrate with parent component props', () => {
|
|
||||||
const parentProps = {
|
|
||||||
entityType: 'projects' as const,
|
|
||||||
routeName: 'project-list',
|
|
||||||
queryParams: { category: 'featured' }
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper = mountComponent(parentProps)
|
|
||||||
|
|
||||||
expect(wrapper.vm.entityType).toBe(parentProps.entityType)
|
|
||||||
expect(wrapper.vm.routeName).toBe(parentProps.routeName)
|
|
||||||
expect(wrapper.vm.queryParams).toEqual(parentProps.queryParams)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Mock Integration Testing', () => {
|
|
||||||
it('should work with simple mock', () => {
|
|
||||||
const mock = new ShowAllCardSimpleMock()
|
|
||||||
expect(mock.navigationRoute).toEqual({
|
|
||||||
name: 'contacts',
|
|
||||||
query: {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should work with standard mock', () => {
|
|
||||||
const mock = new ShowAllCardStandardMock({
|
|
||||||
entityType: 'projects',
|
|
||||||
routeName: 'projects'
|
|
||||||
})
|
|
||||||
expect(mock.getEntityType()).toBe('projects')
|
|
||||||
expect(mock.getRouteName()).toBe('projects')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should work with complex mock', () => {
|
|
||||||
const mock = new ShowAllCardComplexMock({
|
|
||||||
entityType: 'people',
|
|
||||||
routeName: 'contacts',
|
|
||||||
queryParams: { filter: 'active' }
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(mock.isValidState()).toBe(true)
|
|
||||||
expect(mock.getValidationErrors()).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should work with factory functions', () => {
|
|
||||||
const peopleMock = createPeopleShowAllCardMock()
|
|
||||||
const projectsMock = createProjectsShowAllCardMock()
|
|
||||||
|
|
||||||
expect(peopleMock.getEntityType()).toBe('people')
|
|
||||||
expect(projectsMock.getEntityType()).toBe('projects')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should work with complex query mock', () => {
|
|
||||||
const mock = createShowAllCardMockWithComplexQuery()
|
|
||||||
expect(mock.getQueryParams()).toHaveProperty('filter')
|
|
||||||
expect(mock.getQueryParams()).toHaveProperty('sort')
|
|
||||||
expect(mock.getQueryParams()).toHaveProperty('page')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Snapshot Testing', () => {
|
|
||||||
it('should maintain consistent DOM structure', () => {
|
|
||||||
expect(wrapper.html()).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should maintain consistent structure with different props', () => {
|
|
||||||
wrapper = mountComponent({ entityType: 'projects' })
|
|
||||||
expect(wrapper.html()).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should maintain consistent structure with query params', () => {
|
|
||||||
wrapper = mountComponent({
|
|
||||||
queryParams: { filter: 'active', sort: 'name' }
|
|
||||||
})
|
|
||||||
expect(wrapper.html()).toMatchSnapshot()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
/**
|
|
||||||
* ImageViewer Component Mock
|
|
||||||
*
|
|
||||||
* Comprehensive mock implementation for ImageViewer component testing.
|
|
||||||
* Provides multiple mock levels for different testing scenarios and
|
|
||||||
* behavior-focused test patterns.
|
|
||||||
*
|
|
||||||
* @author Matthew Raymer
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { vi } from "vitest";
|
|
||||||
import { Component } from "vue";
|
|
||||||
import { mount, VueWrapper } from "@vue/test-utils";
|
|
||||||
|
|
||||||
// Mock data factories
|
|
||||||
export const createMockImageData = (overrides = {}) => ({
|
|
||||||
imageUrl: "https://example.com/test-image.jpg",
|
|
||||||
imageData: null,
|
|
||||||
isOpen: true,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createMockUserAgent = (overrides = {}) => ({
|
|
||||||
getOS: () => ({ name: "iOS", version: "15.0" }),
|
|
||||||
getBrowser: () => ({ name: "Safari", version: "15.0" }),
|
|
||||||
getDevice: () => ({ type: "mobile", model: "iPhone" }),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createMockNavigator = (overrides = {}) => ({
|
|
||||||
share: vi.fn().mockResolvedValue(undefined),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createMockWindow = (overrides = {}) => ({
|
|
||||||
open: vi.fn(),
|
|
||||||
URL: {
|
|
||||||
createObjectURL: vi.fn().mockReturnValue("blob:mock-url"),
|
|
||||||
revokeObjectURL: vi.fn(),
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simple mock for basic component testing
|
|
||||||
export const createSimpleImageViewerMock = () => {
|
|
||||||
return {
|
|
||||||
template: `
|
|
||||||
<div class="image-viewer-mock">
|
|
||||||
<div class="mock-overlay" v-if="isOpen">
|
|
||||||
<img :src="imageUrl" alt="mock image" />
|
|
||||||
<button @click="close">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
imageUrl: { type: String, required: true },
|
|
||||||
imageData: { type: Object, default: null },
|
|
||||||
isOpen: { type: Boolean, default: false },
|
|
||||||
},
|
|
||||||
emits: ["update:isOpen"],
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.$emit("update:isOpen", false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Standard mock with realistic behavior
|
|
||||||
export const createStandardImageViewerMock = () => {
|
|
||||||
return {
|
|
||||||
template: `
|
|
||||||
<div v-if="isOpen" class="fixed inset-0 z-50 bg-black/90 flex items-center justify-center">
|
|
||||||
<div class="relative max-w-4xl max-h-[calc(100vh-5rem)] p-4">
|
|
||||||
<div class="flex justify-between items-start mb-4">
|
|
||||||
<button
|
|
||||||
data-testid="close-button"
|
|
||||||
@click="close"
|
|
||||||
class="text-white hover:text-gray-300 transition-colors"
|
|
||||||
>
|
|
||||||
<span class="fa-icon">xmark</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMobile"
|
|
||||||
data-testid="share-button"
|
|
||||||
@click="handleShare"
|
|
||||||
class="text-white hover:text-gray-300 transition-colors"
|
|
||||||
>
|
|
||||||
<span class="fa-icon">ellipsis</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
data-testid="viewer-image"
|
|
||||||
:src="imageUrl"
|
|
||||||
alt="expanded shared content"
|
|
||||||
@click="close"
|
|
||||||
class="max-h-[calc(100vh-5rem)] object-contain cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
imageUrl: { type: String, required: true },
|
|
||||||
imageData: { type: Object, default: null },
|
|
||||||
isOpen: { type: Boolean, default: false },
|
|
||||||
},
|
|
||||||
emits: ["update:isOpen"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
userAgent: createMockUserAgent({ getOS: () => ({ name: "Windows" }) }), // Default to desktop
|
|
||||||
shareSuccess: false,
|
|
||||||
shareError: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isMobile() {
|
|
||||||
const os = this.userAgent.getOS().name;
|
|
||||||
return os === "iOS" || os === "Android";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.$emit("update:isOpen", false);
|
|
||||||
},
|
|
||||||
async handleShare() {
|
|
||||||
try {
|
|
||||||
if (navigator.share) {
|
|
||||||
await navigator.share({ url: this.imageUrl });
|
|
||||||
this.shareSuccess = true;
|
|
||||||
} else {
|
|
||||||
window.open(this.imageUrl, "_blank");
|
|
||||||
this.shareSuccess = true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.shareError = error;
|
|
||||||
window.open(this.imageUrl, "_blank");
|
|
||||||
this.shareSuccess = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Complex mock with edge cases and error scenarios
|
|
||||||
export const createComplexImageViewerMock = () => {
|
|
||||||
return {
|
|
||||||
template: `
|
|
||||||
<Teleport to="body">
|
|
||||||
<Transition name="fade">
|
|
||||||
<div v-if="isOpen" class="fixed inset-0 z-50 bg-black/90 flex items-center justify-center">
|
|
||||||
<div class="relative max-w-4xl max-h-[calc(100vh-5rem)] p-4">
|
|
||||||
<div class="flex justify-between items-start mb-4">
|
|
||||||
<button
|
|
||||||
data-testid="close-button"
|
|
||||||
@click="close"
|
|
||||||
:disabled="isClosing"
|
|
||||||
class="text-white hover:text-gray-300 transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<span class="fa-icon">xmark</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMobile"
|
|
||||||
data-testid="share-button"
|
|
||||||
@click="handleShare"
|
|
||||||
:disabled="isSharing"
|
|
||||||
class="text-white hover:text-gray-300 transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<span class="fa-icon">ellipsis</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="imageError" class="text-center text-white">
|
|
||||||
<p>Failed to load image</p>
|
|
||||||
<button
|
|
||||||
v-if="canRetry"
|
|
||||||
@click="retryImage"
|
|
||||||
class="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Retry
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
data-testid="viewer-image"
|
|
||||||
:src="imageUrl"
|
|
||||||
alt="expanded shared content"
|
|
||||||
@click="close"
|
|
||||||
@load="handleImageLoad"
|
|
||||||
@error="handleImageError"
|
|
||||||
class="max-h-[calc(100vh-5rem)] object-contain cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
imageUrl: { type: String, required: true },
|
|
||||||
imageData: { type: Object, default: null },
|
|
||||||
isOpen: { type: Boolean, default: false },
|
|
||||||
},
|
|
||||||
emits: ["update:isOpen", "image-load", "image-error"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
userAgent: createMockUserAgent(),
|
|
||||||
shareSuccess: false,
|
|
||||||
shareError: null,
|
|
||||||
imageLoaded: false,
|
|
||||||
imageError: false,
|
|
||||||
loadAttempts: 0,
|
|
||||||
isClosing: false,
|
|
||||||
isSharing: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isMobile() {
|
|
||||||
const os = this.userAgent.getOS().name;
|
|
||||||
return os === "iOS" || os === "Android";
|
|
||||||
},
|
|
||||||
canRetry() {
|
|
||||||
return this.loadAttempts < 3;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.isClosing = true;
|
|
||||||
this.$emit("update:isOpen", false);
|
|
||||||
setTimeout(() => {
|
|
||||||
this.isClosing = false;
|
|
||||||
}, 300);
|
|
||||||
},
|
|
||||||
async handleShare() {
|
|
||||||
this.isSharing = true;
|
|
||||||
try {
|
|
||||||
if (navigator.share) {
|
|
||||||
await navigator.share({ url: this.imageUrl });
|
|
||||||
this.shareSuccess = true;
|
|
||||||
} else {
|
|
||||||
window.open(this.imageUrl, "_blank");
|
|
||||||
this.shareSuccess = true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.shareError = error;
|
|
||||||
window.open(this.imageUrl, "_blank");
|
|
||||||
this.shareSuccess = true;
|
|
||||||
} finally {
|
|
||||||
this.isSharing = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleImageLoad() {
|
|
||||||
this.imageLoaded = true;
|
|
||||||
this.imageError = false;
|
|
||||||
this.$emit("image-load");
|
|
||||||
},
|
|
||||||
handleImageError() {
|
|
||||||
this.imageError = true;
|
|
||||||
this.imageLoaded = false;
|
|
||||||
this.loadAttempts++;
|
|
||||||
this.$emit("image-error");
|
|
||||||
},
|
|
||||||
retryImage() {
|
|
||||||
this.imageError = false;
|
|
||||||
this.imageLoaded = false;
|
|
||||||
this.loadAttempts = 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
imageUrl() {
|
|
||||||
this.imageError = false;
|
|
||||||
this.imageLoaded = false;
|
|
||||||
this.loadAttempts = 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Integration mock for full component behavior testing
|
|
||||||
export const createIntegrationImageViewerMock = () => {
|
|
||||||
return {
|
|
||||||
template: `
|
|
||||||
<Teleport to="body">
|
|
||||||
<Transition name="fade">
|
|
||||||
<div v-if="isOpen" class="fixed inset-0 z-50 bg-black/90 flex items-center justify-center">
|
|
||||||
<div class="relative max-w-4xl max-h-[calc(100vh-5rem)] p-4">
|
|
||||||
<div class="flex justify-between items-start mb-4">
|
|
||||||
<button
|
|
||||||
data-testid="close-button"
|
|
||||||
@click="close"
|
|
||||||
class="text-white hover:text-gray-300 transition-colors"
|
|
||||||
>
|
|
||||||
<span class="fa-icon">xmark</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMobile"
|
|
||||||
data-testid="share-button"
|
|
||||||
@click="handleShare"
|
|
||||||
class="text-white hover:text-gray-300 transition-colors"
|
|
||||||
>
|
|
||||||
<span class="fa-icon">ellipsis</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
data-testid="viewer-image"
|
|
||||||
:src="imageUrl"
|
|
||||||
alt="expanded shared content"
|
|
||||||
@click="close"
|
|
||||||
@load="handleImageLoad"
|
|
||||||
@error="handleImageError"
|
|
||||||
class="max-h-[calc(100vh-5rem)] object-contain cursor-pointer"
|
|
||||||
/>
|
|
||||||
<!-- Analytics tracking element -->
|
|
||||||
<div data-testid="analytics" style="display: none;">
|
|
||||||
{{ analytics.openCount }} {{ analytics.closeCount }} {{ analytics.shareCount }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
imageUrl: { type: String, required: true },
|
|
||||||
imageData: { type: Object, default: null },
|
|
||||||
isOpen: { type: Boolean, default: false },
|
|
||||||
},
|
|
||||||
emits: ["update:isOpen", "image-load", "image-error", "share-success", "analytics"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
userAgent: createMockUserAgent(),
|
|
||||||
shareSuccess: false,
|
|
||||||
shareError: null,
|
|
||||||
imageLoaded: false,
|
|
||||||
imageError: false,
|
|
||||||
analytics: {
|
|
||||||
openCount: 0,
|
|
||||||
closeCount: 0,
|
|
||||||
shareCount: 0,
|
|
||||||
errorCount: 0,
|
|
||||||
loadTime: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isMobile() {
|
|
||||||
const os = this.userAgent.getOS().name;
|
|
||||||
return os === "iOS" || os === "Android";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.analytics.closeCount++;
|
|
||||||
this.$emit("update:isOpen", false);
|
|
||||||
this.$emit("analytics", this.analytics);
|
|
||||||
},
|
|
||||||
async handleShare() {
|
|
||||||
this.analytics.shareCount++;
|
|
||||||
try {
|
|
||||||
if (navigator.share) {
|
|
||||||
await navigator.share({ url: this.imageUrl });
|
|
||||||
this.shareSuccess = true;
|
|
||||||
this.$emit("share-success");
|
|
||||||
} else {
|
|
||||||
window.open(this.imageUrl, "_blank");
|
|
||||||
this.shareSuccess = true;
|
|
||||||
this.$emit("share-success");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.shareError = error;
|
|
||||||
this.analytics.errorCount++;
|
|
||||||
window.open(this.imageUrl, "_blank");
|
|
||||||
this.shareSuccess = true;
|
|
||||||
this.$emit("share-success");
|
|
||||||
}
|
|
||||||
this.$emit("analytics", this.analytics);
|
|
||||||
},
|
|
||||||
handleImageLoad() {
|
|
||||||
this.imageLoaded = true;
|
|
||||||
this.imageError = false;
|
|
||||||
this.$emit("image-load");
|
|
||||||
},
|
|
||||||
handleImageError() {
|
|
||||||
this.imageError = true;
|
|
||||||
this.imageLoaded = false;
|
|
||||||
this.analytics.errorCount++;
|
|
||||||
this.$emit("image-error");
|
|
||||||
this.$emit("analytics", this.analytics);
|
|
||||||
},
|
|
||||||
getAnalytics() {
|
|
||||||
return this.analytics;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
isOpen(newVal) {
|
|
||||||
if (newVal) {
|
|
||||||
this.analytics.openCount++;
|
|
||||||
this.$emit("analytics", this.analytics);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
// Initialize analytics when component is mounted
|
|
||||||
if (this.isOpen) {
|
|
||||||
this.analytics.openCount++;
|
|
||||||
this.$emit("analytics", this.analytics);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock component wrapper factory
|
|
||||||
export const createImageViewerMockWrapper = (
|
|
||||||
mockLevel: "simple" | "standard" | "complex" | "integration" = "standard"
|
|
||||||
) => {
|
|
||||||
let mockComponent: any;
|
|
||||||
|
|
||||||
switch (mockLevel) {
|
|
||||||
case "simple":
|
|
||||||
mockComponent = createSimpleImageViewerMock();
|
|
||||||
break;
|
|
||||||
case "standard":
|
|
||||||
mockComponent = createStandardImageViewerMock();
|
|
||||||
break;
|
|
||||||
case "complex":
|
|
||||||
mockComponent = createComplexImageViewerMock();
|
|
||||||
break;
|
|
||||||
case "integration":
|
|
||||||
mockComponent = createIntegrationImageViewerMock();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
mockComponent = createStandardImageViewerMock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (props = {}, globalOptions = {}) => {
|
|
||||||
return mount(mockComponent, {
|
|
||||||
props,
|
|
||||||
global: {
|
|
||||||
stubs: {
|
|
||||||
"font-awesome": {
|
|
||||||
template: '<span class="fa-icon">{{ icon }}</span>',
|
|
||||||
props: ["icon"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...globalOptions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test scenarios and data
|
|
||||||
export const createImageViewerTestScenarios = () => ({
|
|
||||||
basic: {
|
|
||||||
props: createMockImageData(),
|
|
||||||
expectedBehavior: "Component renders with basic props",
|
|
||||||
},
|
|
||||||
mobile: {
|
|
||||||
props: createMockImageData({ isOpen: true }),
|
|
||||||
userAgent: createMockUserAgent({ getOS: () => ({ name: "iOS" }) }),
|
|
||||||
expectedBehavior: "Share button visible on mobile",
|
|
||||||
},
|
|
||||||
desktop: {
|
|
||||||
props: createMockImageData({ isOpen: true }),
|
|
||||||
userAgent: createMockUserAgent({ getOS: () => ({ name: "Windows" }) }),
|
|
||||||
expectedBehavior: "Share button hidden on desktop",
|
|
||||||
},
|
|
||||||
imageLoading: {
|
|
||||||
props: createMockImageData({ isOpen: true }),
|
|
||||||
expectedBehavior: "Image loads successfully",
|
|
||||||
},
|
|
||||||
imageError: {
|
|
||||||
props: createMockImageData({ imageUrl: "invalid-url", isOpen: true }),
|
|
||||||
expectedBehavior: "Image error handled gracefully",
|
|
||||||
},
|
|
||||||
shareSuccess: {
|
|
||||||
props: createMockImageData({ isOpen: true }),
|
|
||||||
userAgent: createMockUserAgent({ getOS: () => ({ name: "iOS" }) }),
|
|
||||||
expectedBehavior: "Share API works correctly",
|
|
||||||
},
|
|
||||||
shareFallback: {
|
|
||||||
props: createMockImageData({ isOpen: true }),
|
|
||||||
userAgent: createMockUserAgent({ getOS: () => ({ name: "iOS" }) }),
|
|
||||||
expectedBehavior: "Falls back to window.open",
|
|
||||||
},
|
|
||||||
shareError: {
|
|
||||||
props: createMockImageData({ isOpen: true }),
|
|
||||||
userAgent: createMockUserAgent({ getOS: () => ({ name: "iOS" }) }),
|
|
||||||
expectedBehavior: "Share error handled gracefully",
|
|
||||||
},
|
|
||||||
accessibility: {
|
|
||||||
props: createMockImageData({ isOpen: true }),
|
|
||||||
expectedBehavior: "Proper ARIA labels and keyboard navigation",
|
|
||||||
},
|
|
||||||
performance: {
|
|
||||||
props: createMockImageData({ isOpen: true }),
|
|
||||||
expectedBehavior: "Fast rendering and smooth transitions",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Export default mock for easy import
|
|
||||||
export default createStandardImageViewerMock();
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
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 `<img src="${this.imageUrl}" class="${this.imageClasses}" />`;
|
|
||||||
} else {
|
|
||||||
return `<svg class="jdenticon" width="${this.iconSize}" height="${this.iconSize}"></svg>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,535 +0,0 @@
|
|||||||
# Component Mock Units Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This directory contains comprehensive mock units for Vue component testing,
|
|
||||||
designed for behavior-focused testing patterns. The mocks provide multiple
|
|
||||||
levels of complexity to support different testing scenarios and requirements.
|
|
||||||
|
|
||||||
## Mock Architecture
|
|
||||||
|
|
||||||
### Mock Levels Pattern
|
|
||||||
|
|
||||||
All component mocks follow a consistent 4-level architecture:
|
|
||||||
|
|
||||||
#### 1. Simple Mock (`createSimple[Component]Mock`)
|
|
||||||
**Use Case**: Basic component testing, prop validation, minimal functionality
|
|
||||||
- Basic template with minimal structure
|
|
||||||
- Essential props and events
|
|
||||||
- No complex behavior simulation
|
|
||||||
- Fast execution for quick tests
|
|
||||||
|
|
||||||
#### 2. Standard Mock (`createStandard[Component]Mock`)
|
|
||||||
**Use Case**: Most component testing scenarios, realistic behavior
|
|
||||||
- Full template with realistic structure
|
|
||||||
- Platform detection and feature simulation
|
|
||||||
- Realistic user interactions
|
|
||||||
- Balanced performance and functionality
|
|
||||||
|
|
||||||
#### 3. Complex Mock (`createComplex[Component]Mock`)
|
|
||||||
**Use Case**: Error handling, edge cases, advanced scenarios
|
|
||||||
- Error state simulation
|
|
||||||
- Retry functionality
|
|
||||||
- Loading state management
|
|
||||||
- Error event emissions
|
|
||||||
|
|
||||||
#### 4. Integration Mock (`createIntegration[Component]Mock`)
|
|
||||||
**Use Case**: Full workflow testing, analytics, performance monitoring
|
|
||||||
- Complete user workflow simulation
|
|
||||||
- Analytics tracking
|
|
||||||
- Performance monitoring
|
|
||||||
- Comprehensive event handling
|
|
||||||
|
|
||||||
## Mock Data Factories
|
|
||||||
|
|
||||||
### Standard Factory Pattern
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Generic mock data factory
|
|
||||||
export const createMock[Component]Data = (overrides = {}) => ({
|
|
||||||
// Default props
|
|
||||||
prop1: "default-value",
|
|
||||||
prop2: false,
|
|
||||||
// Component-specific defaults
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Platform-specific factories
|
|
||||||
export const createMockUserAgent = (overrides = {}) => ({
|
|
||||||
getOS: () => ({ name: "iOS", version: "15.0" }),
|
|
||||||
getBrowser: () => ({ name: "Safari", version: "15.0" }),
|
|
||||||
getDevice: () => ({ type: "mobile", model: "iPhone" }),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
// API mocks
|
|
||||||
export const createMockNavigator = (overrides = {}) => ({
|
|
||||||
share: jest.fn().mockResolvedValue(undefined),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createMockWindow = (overrides = {}) => ({
|
|
||||||
open: jest.fn(),
|
|
||||||
URL: {
|
|
||||||
createObjectURL: jest.fn().mockReturnValue("blob:mock-url"),
|
|
||||||
revokeObjectURL: jest.fn(),
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Component Mock Template
|
|
||||||
|
|
||||||
### Basic Structure
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* [Component] Component Mock
|
|
||||||
*
|
|
||||||
* Comprehensive mock implementation for [Component] component testing.
|
|
||||||
* Provides multiple mock levels for different testing scenarios and
|
|
||||||
* behavior-focused test patterns.
|
|
||||||
*
|
|
||||||
* @author Matthew Raymer
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Component } from "vue";
|
|
||||||
import { mount, VueWrapper } from "@vue/test-utils";
|
|
||||||
|
|
||||||
// Mock data factories
|
|
||||||
export const createMock[Component]Data = (overrides = {}) => ({
|
|
||||||
// Component-specific defaults
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simple mock for basic component testing
|
|
||||||
export const createSimple[Component]Mock = () => {
|
|
||||||
return {
|
|
||||||
template: `
|
|
||||||
<div class="[component]-mock">
|
|
||||||
<!-- Basic template structure -->
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
// Component props
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
methods: {
|
|
||||||
// Basic methods
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Standard mock with realistic behavior
|
|
||||||
export const createStandard[Component]Mock = () => {
|
|
||||||
return {
|
|
||||||
template: `
|
|
||||||
<!-- Full template with realistic structure -->
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
// Required props
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue", "custom-event"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
// Component state
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
// Computed properties
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// Component methods
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Complex mock with edge cases and error scenarios
|
|
||||||
export const createComplex[Component]Mock = () => {
|
|
||||||
return {
|
|
||||||
template: `
|
|
||||||
<!-- Template with error handling -->
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
// Component props
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue", "error", "success"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
// State including error handling
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
// Computed properties
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// Methods with error handling
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// Watchers for state changes
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Integration mock for full component behavior testing
|
|
||||||
export const createIntegration[Component]Mock = () => {
|
|
||||||
return {
|
|
||||||
template: `
|
|
||||||
<!-- Full template with analytics -->
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
// Component props
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue", "analytics", "performance"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
// State with analytics tracking
|
|
||||||
analytics: {
|
|
||||||
// Analytics data
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
// Computed properties
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// Methods with analytics
|
|
||||||
getAnalytics() {
|
|
||||||
return this.analytics;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// Watchers for analytics
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock component wrapper factory
|
|
||||||
export const create[Component]MockWrapper = (
|
|
||||||
mockLevel: "simple" | "standard" | "complex" | "integration" = "standard"
|
|
||||||
) => {
|
|
||||||
let mockComponent: any;
|
|
||||||
|
|
||||||
switch (mockLevel) {
|
|
||||||
case "simple":
|
|
||||||
mockComponent = createSimple[Component]Mock();
|
|
||||||
break;
|
|
||||||
case "standard":
|
|
||||||
mockComponent = createStandard[Component]Mock();
|
|
||||||
break;
|
|
||||||
case "complex":
|
|
||||||
mockComponent = createComplex[Component]Mock();
|
|
||||||
break;
|
|
||||||
case "integration":
|
|
||||||
mockComponent = createIntegration[Component]Mock();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
mockComponent = createStandard[Component]Mock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (props = {}, globalOptions = {}) => {
|
|
||||||
return mount(mockComponent, {
|
|
||||||
props,
|
|
||||||
global: {
|
|
||||||
stubs: {
|
|
||||||
// Common stubs
|
|
||||||
},
|
|
||||||
...globalOptions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test scenarios
|
|
||||||
export const create[Component]TestScenarios = () => ({
|
|
||||||
basic: {
|
|
||||||
props: createMock[Component]Data(),
|
|
||||||
expectedBehavior: "Component renders with basic props",
|
|
||||||
},
|
|
||||||
// Additional scenarios
|
|
||||||
});
|
|
||||||
|
|
||||||
// Export default mock for easy import
|
|
||||||
export default createStandard[Component]Mock();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage Patterns
|
|
||||||
|
|
||||||
### 1. Basic Component Testing
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
describe("Basic Component Testing", () => {
|
|
||||||
it("renders with basic props", () => {
|
|
||||||
const createWrapper = create[Component]MockWrapper("simple");
|
|
||||||
const wrapper = createWrapper({
|
|
||||||
prop1: "test-value",
|
|
||||||
prop2: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
expect(wrapper.find(".component-mock").exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Platform-Specific Testing
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
describe("Platform Detection", () => {
|
|
||||||
it("shows platform-specific features", () => {
|
|
||||||
const createWrapper = create[Component]MockWrapper("standard");
|
|
||||||
const wrapper = createWrapper(createMock[Component]Data());
|
|
||||||
|
|
||||||
wrapper.vm.userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "iOS" })
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.vm.isMobile).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Error Scenario Testing
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
describe("Error Handling", () => {
|
|
||||||
it("handles API failures gracefully", async () => {
|
|
||||||
const createWrapper = create[Component]MockWrapper("standard");
|
|
||||||
const mockApi = vi.fn().mockRejectedValue(new Error("API failed"));
|
|
||||||
|
|
||||||
const wrapper = createWrapper(createMock[Component]Data());
|
|
||||||
|
|
||||||
// Trigger error scenario
|
|
||||||
await wrapper.vm.handleApiCall();
|
|
||||||
|
|
||||||
expect(mockApi).toHaveBeenCalled();
|
|
||||||
expect(wrapper.vm.hasError).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Integration Testing
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
describe("Full User Workflow", () => {
|
|
||||||
it("completes full user journey", async () => {
|
|
||||||
const createWrapper = create[Component]MockWrapper("integration");
|
|
||||||
const wrapper = createWrapper(createMock[Component]Data({ isOpen: false }));
|
|
||||||
|
|
||||||
// Step 1: Initialize
|
|
||||||
await wrapper.setProps({ isOpen: true });
|
|
||||||
expect(wrapper.vm.getAnalytics().openCount).toBe(1);
|
|
||||||
|
|
||||||
// Step 2: User interaction
|
|
||||||
const button = wrapper.find('[data-testid="action-button"]');
|
|
||||||
await button.trigger("click");
|
|
||||||
|
|
||||||
// Step 3: Verify results
|
|
||||||
expect(wrapper.vm.getAnalytics().actionCount).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### 1. Choose Appropriate Mock Level
|
|
||||||
|
|
||||||
- Use **simple** for basic prop validation and rendering tests
|
|
||||||
- Use **standard** for most component behavior tests
|
|
||||||
- Use **complex** for error handling and edge case tests
|
|
||||||
- Use **integration** for full workflow and analytics tests
|
|
||||||
|
|
||||||
### 2. Mock Global Objects
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
beforeEach(() => {
|
|
||||||
mockNavigator = createMockNavigator();
|
|
||||||
mockWindow = createMockWindow();
|
|
||||||
global.navigator = mockNavigator;
|
|
||||||
global.window = mockWindow;
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test Platform Detection
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const platforms = [
|
|
||||||
{ name: "iOS", expected: true },
|
|
||||||
{ name: "Android", expected: true },
|
|
||||||
{ name: "Windows", expected: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
platforms.forEach(({ name, expected }) => {
|
|
||||||
wrapper.vm.userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name, version: "1.0" }),
|
|
||||||
});
|
|
||||||
expect(wrapper.vm.isMobile).toBe(expected);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Test Error Scenarios
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Test API failure
|
|
||||||
const mockApi = vi.fn().mockRejectedValue(new Error("API failed"));
|
|
||||||
mockNavigator.share = mockApi;
|
|
||||||
|
|
||||||
// Test component error
|
|
||||||
const element = wrapper.find('[data-testid="component-element"]');
|
|
||||||
await element.trigger("error");
|
|
||||||
expect(wrapper.vm.hasError).toBe(true);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Use Test Data Factories
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Instead of hardcoded data
|
|
||||||
const wrapper = createWrapper({
|
|
||||||
prop1: "test-value",
|
|
||||||
prop2: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use factory functions
|
|
||||||
const wrapper = createWrapper(createMock[Component]Data({
|
|
||||||
prop1: "test-value",
|
|
||||||
prop2: true,
|
|
||||||
}));
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
### 1. Mock Level Performance
|
|
||||||
|
|
||||||
- **Simple**: Fastest execution, minimal overhead
|
|
||||||
- **Standard**: Good balance of features and performance
|
|
||||||
- **Complex**: Moderate overhead for error handling
|
|
||||||
- **Integration**: Highest overhead for analytics tracking
|
|
||||||
|
|
||||||
### 2. Test Execution Tips
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Use simple mock for quick tests
|
|
||||||
const createWrapper = create[Component]MockWrapper("simple");
|
|
||||||
|
|
||||||
// Use standard mock for most tests
|
|
||||||
const createWrapper = create[Component]MockWrapper("standard");
|
|
||||||
|
|
||||||
// Use complex/integration only when needed
|
|
||||||
const createWrapper = create[Component]MockWrapper("complex");
|
|
||||||
```
|
|
||||||
|
|
||||||
## Accessibility Testing
|
|
||||||
|
|
||||||
### 1. ARIA Labels
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it("has proper ARIA labels", () => {
|
|
||||||
const wrapper = createWrapper(createMock[Component]Data());
|
|
||||||
const element = wrapper.find('[data-testid="component-element"]');
|
|
||||||
expect(element.attributes("alt")).toBe("descriptive text");
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Keyboard Navigation
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
it("supports keyboard navigation", async () => {
|
|
||||||
const wrapper = createWrapper(createMock[Component]Data());
|
|
||||||
const button = wrapper.find('[data-testid="action-button"]');
|
|
||||||
|
|
||||||
await button.trigger("keydown.enter");
|
|
||||||
expect(wrapper.emitted("action")).toBeTruthy();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **Mock not found**: Ensure proper import path
|
|
||||||
```typescript
|
|
||||||
import { create[Component]MockWrapper } from "./__mocks__/[Component].mock";
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Global objects not mocked**: Set up in beforeEach
|
|
||||||
```typescript
|
|
||||||
beforeEach(() => {
|
|
||||||
global.navigator = createMockNavigator();
|
|
||||||
global.window = createMockWindow();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **User agent not working**: Set userAgent property directly
|
|
||||||
```typescript
|
|
||||||
wrapper.vm.userAgent = createMockUserAgent({
|
|
||||||
getOS: () => ({ name: "iOS" })
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Events not emitting**: Use async/await for event triggers
|
|
||||||
```typescript
|
|
||||||
await button.trigger("click");
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Tips
|
|
||||||
|
|
||||||
1. **Check mock level**: Verify you're using the right mock level
|
|
||||||
2. **Inspect wrapper**: Use `console.log(wrapper.html())` to see rendered output
|
|
||||||
3. **Check props**: Use `console.log(wrapper.props())` to verify prop values
|
|
||||||
4. **Monitor events**: Use `console.log(wrapper.emitted())` to see emitted events
|
|
||||||
|
|
||||||
## Migration from Legacy Tests
|
|
||||||
|
|
||||||
### Before (Legacy)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Old way - direct component testing
|
|
||||||
const wrapper = mount(Component, {
|
|
||||||
props: { prop1: "test", prop2: true },
|
|
||||||
global: { stubs: { "font-awesome": true } }
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### After (Mock Units)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// New way - behavior-focused testing
|
|
||||||
const createWrapper = create[Component]MockWrapper("standard");
|
|
||||||
const wrapper = createWrapper(createMock[Component]Data({ prop1: "test" }));
|
|
||||||
|
|
||||||
// Test behavior, not implementation
|
|
||||||
expect(wrapper.vm.isMobile).toBe(false);
|
|
||||||
expect(wrapper.find('[data-testid="feature"]').exists()).toBe(false);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
When adding new mocks or updating existing ones:
|
|
||||||
|
|
||||||
1. **Follow naming conventions**: Use descriptive names with `create` prefix
|
|
||||||
2. **Add documentation**: Include JSDoc comments for all functions
|
|
||||||
3. **Test all levels**: Ensure all mock levels work correctly
|
|
||||||
4. **Update examples**: Add usage examples for new features
|
|
||||||
5. **Maintain consistency**: Follow existing patterns and structure
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- Mocks should not expose sensitive data
|
|
||||||
- Use realistic but safe test data
|
|
||||||
- Avoid hardcoded credentials or tokens
|
|
||||||
- Sanitize any user-provided data in mocks
|
|
||||||
|
|
||||||
## Example: ImageViewer Implementation
|
|
||||||
|
|
||||||
The `ImageViewer.mock.ts` file demonstrates this pattern in practice:
|
|
||||||
|
|
||||||
- **4 mock levels** with increasing complexity
|
|
||||||
- **Mock data factories** for realistic test data
|
|
||||||
- **Platform detection** for mobile vs desktop testing
|
|
||||||
- **Error handling** for share API and image loading failures
|
|
||||||
- **Analytics tracking** for performance monitoring
|
|
||||||
- **Comprehensive tests** showing all usage patterns
|
|
||||||
|
|
||||||
This serves as a template for creating mocks for other components in the project.
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RegistrationNotice Mock Component
|
|
||||||
*
|
|
||||||
* A mock implementation of the RegistrationNotice 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: "RegistrationNotice" })
|
|
||||||
export default class RegistrationNoticeMock extends Vue {
|
|
||||||
@Prop({ required: true }) isRegistered!: boolean;
|
|
||||||
@Prop({ required: true }) show!: boolean;
|
|
||||||
|
|
||||||
@Emit("share-info")
|
|
||||||
shareInfo() {
|
|
||||||
// Mock implementation - just emits the event
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock method to simulate button click for testing
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
mockShareInfoClick(): void {
|
|
||||||
this.shareInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock method to check if component should be visible
|
|
||||||
* @returns boolean - true if component should be shown
|
|
||||||
*/
|
|
||||||
get shouldShow(): boolean {
|
|
||||||
return !this.isRegistered && this.show;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock method to get button text
|
|
||||||
* @returns string - the button text
|
|
||||||
*/
|
|
||||||
get buttonText(): string {
|
|
||||||
return "Share Your Info";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock method to get notice text
|
|
||||||
* @returns string - the notice message
|
|
||||||
*/
|
|
||||||
get noticeText(): string {
|
|
||||||
return "Before you can publicly announce a new project or time commitment, a friend needs to register you.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
/**
|
|
||||||
* ShowAllCard Mock Component
|
|
||||||
*
|
|
||||||
* Provides three-tier mock architecture for testing:
|
|
||||||
* - Simple: Basic interface compliance
|
|
||||||
* - Standard: Full interface with realistic behavior
|
|
||||||
* - Complex: Enhanced testing capabilities
|
|
||||||
*
|
|
||||||
* @author Matthew Raymer
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { RouteLocationRaw } from "vue-router";
|
|
||||||
|
|
||||||
export interface ShowAllCardProps {
|
|
||||||
entityType: "people" | "projects";
|
|
||||||
routeName: string;
|
|
||||||
queryParams?: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ShowAllCardMock {
|
|
||||||
props: ShowAllCardProps;
|
|
||||||
navigationRoute: RouteLocationRaw;
|
|
||||||
getCssClasses(): string[];
|
|
||||||
getIconClasses(): string[];
|
|
||||||
getTitleClasses(): string[];
|
|
||||||
simulateClick(): void;
|
|
||||||
simulateHover(): void;
|
|
||||||
getComputedNavigationRoute(): RouteLocationRaw;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple Mock - Basic interface compliance
|
|
||||||
*/
|
|
||||||
export class ShowAllCardSimpleMock implements ShowAllCardMock {
|
|
||||||
props: ShowAllCardProps = {
|
|
||||||
entityType: "people",
|
|
||||||
routeName: "contacts",
|
|
||||||
queryParams: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
get navigationRoute(): RouteLocationRaw {
|
|
||||||
return {
|
|
||||||
name: this.props.routeName,
|
|
||||||
query: this.props.queryParams || {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getCssClasses(): string[] {
|
|
||||||
return ["cursor-pointer"];
|
|
||||||
}
|
|
||||||
|
|
||||||
getIconClasses(): string[] {
|
|
||||||
return ["text-blue-500", "text-5xl", "mb-1"];
|
|
||||||
}
|
|
||||||
|
|
||||||
getTitleClasses(): string[] {
|
|
||||||
return ["text-xs", "text-slate-500", "font-medium", "italic", "text-ellipsis", "whitespace-nowrap", "overflow-hidden"];
|
|
||||||
}
|
|
||||||
|
|
||||||
simulateClick(): void {
|
|
||||||
// Basic click simulation
|
|
||||||
}
|
|
||||||
|
|
||||||
simulateHover(): void {
|
|
||||||
// Basic hover simulation
|
|
||||||
}
|
|
||||||
|
|
||||||
getComputedNavigationRoute(): RouteLocationRaw {
|
|
||||||
return this.navigationRoute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard Mock - Full interface compliance with realistic behavior
|
|
||||||
*/
|
|
||||||
export class ShowAllCardStandardMock extends ShowAllCardSimpleMock {
|
|
||||||
constructor(props?: Partial<ShowAllCardProps>) {
|
|
||||||
super();
|
|
||||||
if (props) {
|
|
||||||
this.props = { ...this.props, ...props };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getCssClasses(): string[] {
|
|
||||||
return [
|
|
||||||
"cursor-pointer",
|
|
||||||
"show-all-card",
|
|
||||||
`entity-type-${this.props.entityType}`
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
getIconClasses(): string[] {
|
|
||||||
return [
|
|
||||||
"text-blue-500",
|
|
||||||
"text-5xl",
|
|
||||||
"mb-1",
|
|
||||||
"fa-circle-right",
|
|
||||||
"transition-transform"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
getTitleClasses(): string[] {
|
|
||||||
return [
|
|
||||||
"text-xs",
|
|
||||||
"text-slate-500",
|
|
||||||
"font-medium",
|
|
||||||
"italic",
|
|
||||||
"text-ellipsis",
|
|
||||||
"whitespace-nowrap",
|
|
||||||
"overflow-hidden",
|
|
||||||
"show-all-title"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
simulateClick(): void {
|
|
||||||
// Simulate router navigation
|
|
||||||
this.getComputedNavigationRoute();
|
|
||||||
}
|
|
||||||
|
|
||||||
simulateHover(): void {
|
|
||||||
// Simulate hover effects
|
|
||||||
this.getIconClasses().push("hover:scale-110");
|
|
||||||
}
|
|
||||||
|
|
||||||
getComputedNavigationRoute(): RouteLocationRaw {
|
|
||||||
return {
|
|
||||||
name: this.props.routeName,
|
|
||||||
query: this.props.queryParams || {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods for test scenarios
|
|
||||||
setEntityType(entityType: "people" | "projects"): void {
|
|
||||||
this.props.entityType = entityType;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRouteName(routeName: string): void {
|
|
||||||
this.props.routeName = routeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
setQueryParams(queryParams: Record<string, string>): void {
|
|
||||||
this.props.queryParams = queryParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEntityType(): string {
|
|
||||||
return this.props.entityType;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRouteName(): string {
|
|
||||||
return this.props.routeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
getQueryParams(): Record<string, string> {
|
|
||||||
return this.props.queryParams || {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Complex Mock - Enhanced testing capabilities
|
|
||||||
*/
|
|
||||||
export class ShowAllCardComplexMock extends ShowAllCardStandardMock {
|
|
||||||
private clickCount: number = 0;
|
|
||||||
private hoverCount: number = 0;
|
|
||||||
private navigationHistory: RouteLocationRaw[] = [];
|
|
||||||
|
|
||||||
constructor(props?: Partial<ShowAllCardProps>) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
simulateClick(): void {
|
|
||||||
this.clickCount++;
|
|
||||||
const route = this.getComputedNavigationRoute();
|
|
||||||
this.navigationHistory.push(route);
|
|
||||||
|
|
||||||
// Simulate click event with additional context
|
|
||||||
this.getIconClasses().push("clicked");
|
|
||||||
}
|
|
||||||
|
|
||||||
simulateHover(): void {
|
|
||||||
this.hoverCount++;
|
|
||||||
this.getIconClasses().push("hovered", "scale-110");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performance testing hooks
|
|
||||||
getClickCount(): number {
|
|
||||||
return this.clickCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHoverCount(): number {
|
|
||||||
return this.hoverCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNavigationHistory(): RouteLocationRaw[] {
|
|
||||||
return [...this.navigationHistory];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error scenario simulation
|
|
||||||
simulateInvalidRoute(): void {
|
|
||||||
this.props.routeName = "invalid-route";
|
|
||||||
}
|
|
||||||
|
|
||||||
simulateEmptyQueryParams(): void {
|
|
||||||
this.props.queryParams = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
simulateComplexQueryParams(): void {
|
|
||||||
this.props.queryParams = {
|
|
||||||
filter: "active",
|
|
||||||
sort: "name",
|
|
||||||
page: "1",
|
|
||||||
limit: "20"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accessibility testing support
|
|
||||||
getAccessibilityAttributes(): Record<string, string> {
|
|
||||||
return {
|
|
||||||
role: "listitem",
|
|
||||||
"aria-label": `Show all ${this.props.entityType}`,
|
|
||||||
tabindex: "0"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// State validation helpers
|
|
||||||
isValidState(): boolean {
|
|
||||||
return !!this.props.entityType &&
|
|
||||||
!!this.props.routeName &&
|
|
||||||
typeof this.props.queryParams === "object";
|
|
||||||
}
|
|
||||||
|
|
||||||
getValidationErrors(): string[] {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
if (!this.props.entityType) {
|
|
||||||
errors.push("entityType is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.props.routeName) {
|
|
||||||
errors.push("routeName is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.queryParams && typeof this.props.queryParams !== "object") {
|
|
||||||
errors.push("queryParams must be an object");
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset functionality for test isolation
|
|
||||||
reset(): void {
|
|
||||||
this.clickCount = 0;
|
|
||||||
this.hoverCount = 0;
|
|
||||||
this.navigationHistory = [];
|
|
||||||
this.props = {
|
|
||||||
entityType: "people",
|
|
||||||
routeName: "contacts",
|
|
||||||
queryParams: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default export for convenience
|
|
||||||
export default ShowAllCardComplexMock;
|
|
||||||
|
|
||||||
// Factory functions for common test scenarios
|
|
||||||
export const createShowAllCardMock = (props?: Partial<ShowAllCardProps>): ShowAllCardComplexMock => {
|
|
||||||
return new ShowAllCardComplexMock(props);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createPeopleShowAllCardMock = (): ShowAllCardComplexMock => {
|
|
||||||
return new ShowAllCardComplexMock({
|
|
||||||
entityType: "people",
|
|
||||||
routeName: "contacts",
|
|
||||||
queryParams: { filter: "all" }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createProjectsShowAllCardMock = (): ShowAllCardComplexMock => {
|
|
||||||
return new ShowAllCardComplexMock({
|
|
||||||
entityType: "projects",
|
|
||||||
routeName: "projects",
|
|
||||||
queryParams: { sort: "name" }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createShowAllCardMockWithComplexQuery = (): ShowAllCardComplexMock => {
|
|
||||||
return new ShowAllCardComplexMock({
|
|
||||||
entityType: "people",
|
|
||||||
routeName: "contacts",
|
|
||||||
queryParams: {
|
|
||||||
filter: "active",
|
|
||||||
sort: "name",
|
|
||||||
page: "1",
|
|
||||||
limit: "20",
|
|
||||||
search: "test"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`ShowAllCard > Snapshot Testing > should maintain consistent DOM structure 1`] = `
|
|
||||||
"<li data-v-18958371="" class="cursor-pointer">
|
|
||||||
<router-link data-v-18958371="" to="[object Object]" class="block text-center">
|
|
||||||
<font-awesome data-v-18958371="" icon="circle-right" class="text-blue-500 text-5xl mb-1"></font-awesome>
|
|
||||||
<h3 data-v-18958371="" class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"> Show All </h3>
|
|
||||||
</router-link>
|
|
||||||
</li>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`ShowAllCard > Snapshot Testing > should maintain consistent structure with different props 1`] = `
|
|
||||||
"<li data-v-18958371="" class="cursor-pointer">
|
|
||||||
<router-link data-v-18958371="" to="[object Object]" class="block text-center">
|
|
||||||
<font-awesome data-v-18958371="" icon="circle-right" class="text-blue-500 text-5xl mb-1"></font-awesome>
|
|
||||||
<h3 data-v-18958371="" class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"> Show All </h3>
|
|
||||||
</router-link>
|
|
||||||
</li>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`ShowAllCard > Snapshot Testing > should maintain consistent structure with query params 1`] = `
|
|
||||||
"<li data-v-18958371="" class="cursor-pointer">
|
|
||||||
<router-link data-v-18958371="" to="[object Object]" class="block text-center">
|
|
||||||
<font-awesome data-v-18958371="" icon="circle-right" class="text-blue-500 text-5xl mb-1"></font-awesome>
|
|
||||||
<h3 data-v-18958371="" class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"> Show All </h3>
|
|
||||||
</router-link>
|
|
||||||
</li>"
|
|
||||||
`;
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 {
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,437 +0,0 @@
|
|||||||
/**
|
|
||||||
* Enhanced Testing Example
|
|
||||||
*
|
|
||||||
* Demonstrates how to use the expanded test utilities for comprehensive
|
|
||||||
* component testing with factories, mocks, and assertion helpers.
|
|
||||||
*
|
|
||||||
* @author Matthew Raymer
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from "vitest";
|
|
||||||
import { mount } from "@vue/test-utils";
|
|
||||||
import {
|
|
||||||
createTestSetup,
|
|
||||||
createMockApiClient,
|
|
||||||
createMockNotificationService,
|
|
||||||
createMockAuthService,
|
|
||||||
createMockDatabaseService,
|
|
||||||
assertionUtils,
|
|
||||||
componentUtils,
|
|
||||||
lifecycleUtils,
|
|
||||||
watcherUtils,
|
|
||||||
eventModifierUtils,
|
|
||||||
} from "@/test/utils/testHelpers";
|
|
||||||
import {
|
|
||||||
createSimpleMockContact,
|
|
||||||
createStandardMockContact,
|
|
||||||
createComplexMockContact,
|
|
||||||
createMockProject,
|
|
||||||
createMockAccount,
|
|
||||||
createMockUser,
|
|
||||||
createMockSettings,
|
|
||||||
} from "@/test/factories/contactFactory";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example component for testing
|
|
||||||
*/
|
|
||||||
const ExampleComponent = {
|
|
||||||
template: `
|
|
||||||
<div class="example-component">
|
|
||||||
<h1>{{ title }}</h1>
|
|
||||||
<p>{{ description }}</p>
|
|
||||||
<button @click="handleClick" class="btn-primary">
|
|
||||||
{{ buttonText }}
|
|
||||||
</button>
|
|
||||||
<div v-if="showDetails" class="details">
|
|
||||||
<p>{{ details }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
props: {
|
|
||||||
title: { type: String, required: true },
|
|
||||||
description: { type: String, default: "" },
|
|
||||||
buttonText: { type: String, default: "Click Me" },
|
|
||||||
showDetails: { type: Boolean, default: false },
|
|
||||||
details: { type: String, default: "" },
|
|
||||||
},
|
|
||||||
emits: ["click", "details-toggle"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
clickCount: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
displayTitle() {
|
|
||||||
return this.title.toUpperCase();
|
|
||||||
},
|
|
||||||
hasDescription() {
|
|
||||||
return this.description.length > 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleClick() {
|
|
||||||
this.clickCount++;
|
|
||||||
this.$emit("click", this.clickCount);
|
|
||||||
},
|
|
||||||
toggleDetails() {
|
|
||||||
this.$emit("details-toggle", !this.showDetails);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("Enhanced Testing Example", () => {
|
|
||||||
const setup = createTestSetup(ExampleComponent, {
|
|
||||||
title: "Test Component",
|
|
||||||
description: "Test description",
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
setup.wrapper = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Factory Functions Example", () => {
|
|
||||||
it("should demonstrate contact factory usage", () => {
|
|
||||||
// Simple contact for basic testing
|
|
||||||
const simpleContact = createSimpleMockContact();
|
|
||||||
expect(simpleContact.did).toBeDefined();
|
|
||||||
expect(simpleContact.name).toBeDefined();
|
|
||||||
|
|
||||||
// Standard contact for most testing
|
|
||||||
const standardContact = createStandardMockContact();
|
|
||||||
expect(standardContact.contactMethods).toBeDefined();
|
|
||||||
expect(standardContact.notes).toBeDefined();
|
|
||||||
|
|
||||||
// Complex contact for integration testing
|
|
||||||
const complexContact = createComplexMockContact();
|
|
||||||
expect(complexContact.profileImageUrl).toBeDefined();
|
|
||||||
expect(complexContact.publicKeyBase64).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should demonstrate other factory functions", () => {
|
|
||||||
const project = createMockProject({ name: "Test Project" });
|
|
||||||
const account = createMockAccount({ balance: 500.0 });
|
|
||||||
const user = createMockUser({ username: "testuser" });
|
|
||||||
const settings = createMockSettings({ theme: "dark" });
|
|
||||||
|
|
||||||
expect(project.name).toBe("Test Project");
|
|
||||||
expect(account.balance).toBe(500.0);
|
|
||||||
expect(user.username).toBe("testuser");
|
|
||||||
expect(settings.theme).toBe("dark");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Mock Services Example", () => {
|
|
||||||
it("should demonstrate API client mocking", () => {
|
|
||||||
const apiClient = createMockApiClient();
|
|
||||||
|
|
||||||
// Test API methods
|
|
||||||
expect(apiClient.get).toBeDefined();
|
|
||||||
expect(apiClient.post).toBeDefined();
|
|
||||||
expect(apiClient.put).toBeDefined();
|
|
||||||
expect(apiClient.delete).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should demonstrate notification service mocking", () => {
|
|
||||||
const notificationService = createMockNotificationService();
|
|
||||||
|
|
||||||
// Test notification methods
|
|
||||||
expect(notificationService.show).toBeDefined();
|
|
||||||
expect(notificationService.success).toBeDefined();
|
|
||||||
expect(notificationService.error).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should demonstrate auth service mocking", () => {
|
|
||||||
const authService = createMockAuthService();
|
|
||||||
|
|
||||||
// Test auth methods
|
|
||||||
expect(authService.login).toBeDefined();
|
|
||||||
expect(authService.logout).toBeDefined();
|
|
||||||
expect(authService.isAuthenticated).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should demonstrate database service mocking", () => {
|
|
||||||
const dbService = createMockDatabaseService();
|
|
||||||
|
|
||||||
// Test database methods
|
|
||||||
expect(dbService.query).toBeDefined();
|
|
||||||
expect(dbService.execute).toBeDefined();
|
|
||||||
expect(dbService.transaction).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Assertion Utils Example", () => {
|
|
||||||
it("should demonstrate assertion utilities", async () => {
|
|
||||||
const wrapper = mount(ExampleComponent, {
|
|
||||||
props: {
|
|
||||||
title: "Test Title",
|
|
||||||
description: "Test Description",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert required props
|
|
||||||
assertionUtils.assertRequiredProps(wrapper, ["title"]);
|
|
||||||
|
|
||||||
// Assert CSS classes
|
|
||||||
const button = wrapper.find("button");
|
|
||||||
assertionUtils.assertHasClasses(button, ["btn-primary"]);
|
|
||||||
|
|
||||||
// Assert attributes
|
|
||||||
assertionUtils.assertHasAttributes(button, {
|
|
||||||
type: "button",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert accessibility
|
|
||||||
assertionUtils.assertIsAccessible(button);
|
|
||||||
|
|
||||||
// Assert ARIA attributes
|
|
||||||
assertionUtils.assertHasAriaAttributes(button, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should demonstrate performance assertions", async () => {
|
|
||||||
const duration = await assertionUtils.assertPerformance(async () => {
|
|
||||||
const wrapper = mount(ExampleComponent, {
|
|
||||||
props: { title: "Performance Test" },
|
|
||||||
});
|
|
||||||
await wrapper.unmount();
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
expect(duration).toBeLessThan(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should demonstrate error handling assertions", async () => {
|
|
||||||
const invalidProps = [
|
|
||||||
{ title: null },
|
|
||||||
{ title: undefined },
|
|
||||||
{ title: 123 },
|
|
||||||
{ title: {} },
|
|
||||||
];
|
|
||||||
|
|
||||||
await assertionUtils.assertErrorHandling(ExampleComponent, invalidProps);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should demonstrate accessibility compliance", () => {
|
|
||||||
const wrapper = mount(ExampleComponent, {
|
|
||||||
props: { title: "Accessibility Test" },
|
|
||||||
});
|
|
||||||
|
|
||||||
assertionUtils.assertAccessibilityCompliance(wrapper);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Component Utils Example", () => {
|
|
||||||
it("should demonstrate prop combination testing", async () => {
|
|
||||||
const propCombinations = [
|
|
||||||
{ title: "Test 1", showDetails: true },
|
|
||||||
{ title: "Test 2", showDetails: false },
|
|
||||||
{ title: "Test 3", description: "With description" },
|
|
||||||
{ title: "Test 4", buttonText: "Custom Button" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const results = await componentUtils.testPropCombinations(
|
|
||||||
ExampleComponent,
|
|
||||||
propCombinations,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(results).toHaveLength(4);
|
|
||||||
expect(results.every((r) => r.success)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should demonstrate responsive behavior testing", async () => {
|
|
||||||
const results = await componentUtils.testResponsiveBehavior(
|
|
||||||
ExampleComponent,
|
|
||||||
{ title: "Responsive Test" },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(results).toHaveLength(4); // 4 screen sizes
|
|
||||||
expect(results.every((r) => r.rendered)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should demonstrate theme behavior testing", async () => {
|
|
||||||
const results = await componentUtils.testThemeBehavior(ExampleComponent, {
|
|
||||||
title: "Theme Test",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(results).toHaveLength(3); // 3 themes
|
|
||||||
expect(results.every((r) => r.rendered)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should demonstrate internationalization testing", async () => {
|
|
||||||
const results = await componentUtils.testInternationalization(
|
|
||||||
ExampleComponent,
|
|
||||||
{ title: "i18n Test" },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(results).toHaveLength(4); // 4 languages
|
|
||||||
expect(results.every((r) => r.rendered)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Lifecycle Utils Example", () => {
|
|
||||||
it("should demonstrate lifecycle testing", async () => {
|
|
||||||
// Test mounting
|
|
||||||
const wrapper = await lifecycleUtils.testMounting(ExampleComponent, {
|
|
||||||
title: "Lifecycle Test",
|
|
||||||
});
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
|
|
||||||
// Test unmounting
|
|
||||||
await lifecycleUtils.testUnmounting(wrapper);
|
|
||||||
|
|
||||||
// Test prop updates
|
|
||||||
const mountedWrapper = mount(ExampleComponent, { title: "Test" });
|
|
||||||
const propUpdates = [
|
|
||||||
{ props: { title: "Updated Title" } },
|
|
||||||
{ props: { showDetails: true } },
|
|
||||||
{ props: { description: "Updated description" } },
|
|
||||||
];
|
|
||||||
|
|
||||||
const results = await lifecycleUtils.testPropUpdates(
|
|
||||||
mountedWrapper,
|
|
||||||
propUpdates,
|
|
||||||
);
|
|
||||||
expect(results).toHaveLength(3);
|
|
||||||
expect(results.every((r) => r.success)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Computed Utils Example", () => {
|
|
||||||
it("should demonstrate computed property testing", async () => {
|
|
||||||
const wrapper = mount(ExampleComponent, {
|
|
||||||
props: { title: "Computed Test" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test computed property values
|
|
||||||
const vm = wrapper.vm as any;
|
|
||||||
expect(vm.displayTitle).toBe("COMPUTED TEST");
|
|
||||||
expect(vm.hasDescription).toBe(false);
|
|
||||||
|
|
||||||
// Test computed property dependencies
|
|
||||||
await wrapper.setProps({ description: "New description" });
|
|
||||||
expect(vm.hasDescription).toBe(true);
|
|
||||||
|
|
||||||
// Test computed property caching
|
|
||||||
const firstCall = vm.displayTitle;
|
|
||||||
const secondCall = vm.displayTitle;
|
|
||||||
expect(firstCall).toBe(secondCall);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Watcher Utils Example", () => {
|
|
||||||
it("should demonstrate watcher testing", async () => {
|
|
||||||
const wrapper = mount(ExampleComponent, {
|
|
||||||
props: { title: "Watcher Test" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test watcher triggers
|
|
||||||
const result = await watcherUtils.testWatcherTrigger(
|
|
||||||
wrapper,
|
|
||||||
"title",
|
|
||||||
"New Title",
|
|
||||||
);
|
|
||||||
expect(result.triggered).toBe(true);
|
|
||||||
|
|
||||||
// Test watcher cleanup
|
|
||||||
const cleanupResult = await watcherUtils.testWatcherCleanup(wrapper);
|
|
||||||
expect(cleanupResult.unmounted).toBe(true);
|
|
||||||
|
|
||||||
// Test deep watchers
|
|
||||||
const newWrapper = mount(ExampleComponent, { title: "Deep Test" });
|
|
||||||
const deepResult = await watcherUtils.testDeepWatcher(
|
|
||||||
newWrapper,
|
|
||||||
"title",
|
|
||||||
"Deep Title",
|
|
||||||
);
|
|
||||||
expect(deepResult.updated).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Event Modifier Utils Example", () => {
|
|
||||||
it("should demonstrate event modifier testing", async () => {
|
|
||||||
const wrapper = mount(ExampleComponent, {
|
|
||||||
props: { title: "Event Test" },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test prevent modifier
|
|
||||||
const preventResult = await eventModifierUtils.testPreventModifier(
|
|
||||||
wrapper,
|
|
||||||
"button",
|
|
||||||
);
|
|
||||||
expect(preventResult.eventTriggered).toBe(true);
|
|
||||||
expect(preventResult.preventDefaultCalled).toBe(true);
|
|
||||||
|
|
||||||
// Test stop modifier
|
|
||||||
const stopResult = await eventModifierUtils.testStopModifier(
|
|
||||||
wrapper,
|
|
||||||
"button",
|
|
||||||
);
|
|
||||||
expect(stopResult.eventTriggered).toBe(true);
|
|
||||||
expect(stopResult.stopPropagationCalled).toBe(true);
|
|
||||||
|
|
||||||
// Test once modifier
|
|
||||||
const onceResult = await eventModifierUtils.testOnceModifier(
|
|
||||||
wrapper,
|
|
||||||
"button",
|
|
||||||
);
|
|
||||||
expect(onceResult.firstClickEmitted).toBe(true);
|
|
||||||
expect(onceResult.secondClickEmitted).toBe(true);
|
|
||||||
|
|
||||||
// Test self modifier
|
|
||||||
const selfResult = await eventModifierUtils.testSelfModifier(
|
|
||||||
wrapper,
|
|
||||||
"button",
|
|
||||||
);
|
|
||||||
expect(selfResult.selfClickEmitted).toBe(true);
|
|
||||||
expect(selfResult.childClickEmitted).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Integration Example", () => {
|
|
||||||
it("should demonstrate comprehensive testing workflow", async () => {
|
|
||||||
// 1. Create test data using factories
|
|
||||||
const contact = createStandardMockContact();
|
|
||||||
const project = createMockProject();
|
|
||||||
const user = createMockUser();
|
|
||||||
|
|
||||||
// 2. Create mock services
|
|
||||||
const apiClient = createMockApiClient();
|
|
||||||
const notificationService = createMockNotificationService();
|
|
||||||
const authService = createMockAuthService();
|
|
||||||
|
|
||||||
// 3. Mount component with mocks
|
|
||||||
const wrapper = mount(ExampleComponent, {
|
|
||||||
props: { title: "Integration Test" },
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
apiClient,
|
|
||||||
notificationService,
|
|
||||||
authService,
|
|
||||||
contact,
|
|
||||||
project,
|
|
||||||
user,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Run comprehensive assertions
|
|
||||||
assertionUtils.assertRequiredProps(wrapper, ["title"]);
|
|
||||||
assertionUtils.assertIsAccessible(wrapper.find("button"));
|
|
||||||
assertionUtils.assertAccessibilityCompliance(wrapper);
|
|
||||||
|
|
||||||
// 5. Test lifecycle
|
|
||||||
await lifecycleUtils.testUnmounting(wrapper);
|
|
||||||
|
|
||||||
// 6. Test performance
|
|
||||||
await assertionUtils.assertPerformance(async () => {
|
|
||||||
const newWrapper = mount(ExampleComponent, {
|
|
||||||
title: "Performance Test",
|
|
||||||
});
|
|
||||||
await newWrapper.unmount();
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
// 7. Verify all mocks were used correctly
|
|
||||||
expect(apiClient.get).not.toHaveBeenCalled();
|
|
||||||
expect(notificationService.show).not.toHaveBeenCalled();
|
|
||||||
expect(authService.isAuthenticated).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
/**
|
|
||||||
* Contact Factory for TimeSafari Testing
|
|
||||||
*
|
|
||||||
* Provides different levels of mock contact data for testing
|
|
||||||
* 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";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a simple mock contact for basic component testing
|
|
||||||
* Used for: LargeIdenticonModal, EntityIcon, basic display components
|
|
||||||
*/
|
|
||||||
export const createSimpleMockContact = (overrides = {}): Contact => ({
|
|
||||||
did: `did:ethr:test:${Date.now()}`,
|
|
||||||
name: `Test Contact ${Date.now()}`,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a standard mock contact for most component testing
|
|
||||||
* Used for: ContactList, ContactEdit, ContactView components
|
|
||||||
*/
|
|
||||||
export const createStandardMockContact = (overrides = {}): Contact => ({
|
|
||||||
did: `did:ethr:test:${Date.now()}`,
|
|
||||||
name: `Test Contact ${Date.now()}`,
|
|
||||||
contactMethods: [
|
|
||||||
{ label: "Email", type: "EMAIL", value: "test@example.com" },
|
|
||||||
{ label: "Phone", type: "SMS", value: "+1234567890" },
|
|
||||||
],
|
|
||||||
notes: "Test contact notes",
|
|
||||||
seesMe: true,
|
|
||||||
registered: false,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a complex mock contact for integration and service testing
|
|
||||||
* Used for: Full contact management, service integration tests
|
|
||||||
*/
|
|
||||||
export const createComplexMockContact = (overrides = {}): Contact => ({
|
|
||||||
did: `did:ethr:test:${Date.now()}`,
|
|
||||||
name: `Test Contact ${Date.now()}`,
|
|
||||||
contactMethods: [
|
|
||||||
{ label: "Email", type: "EMAIL", value: "test@example.com" },
|
|
||||||
{ label: "Phone", type: "SMS", value: "+1234567890" },
|
|
||||||
{ label: "WhatsApp", type: "WHATSAPP", value: "+1234567890" },
|
|
||||||
],
|
|
||||||
notes: "Test contact notes with special characters: éñü",
|
|
||||||
profileImageUrl: "https://example.com/avatar.jpg",
|
|
||||||
publicKeyBase64: "base64encodedpublickey",
|
|
||||||
nextPubKeyHashB64: "base64encodedhash",
|
|
||||||
seesMe: true,
|
|
||||||
registered: true,
|
|
||||||
iViewContent: true,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create multiple contacts for list testing
|
|
||||||
* @param count - Number of contacts to create
|
|
||||||
* @param factory - Factory function to use (default: standard)
|
|
||||||
* @returns Array of mock contacts
|
|
||||||
*/
|
|
||||||
export const createMockContacts = (
|
|
||||||
count: number,
|
|
||||||
factory = createStandardMockContact,
|
|
||||||
): Contact[] => {
|
|
||||||
return Array.from({ length: count }, (_, index) =>
|
|
||||||
factory({
|
|
||||||
did: `did:ethr:test:${index + 1}`,
|
|
||||||
name: `Test Contact ${index + 1}`,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create invalid contact data for error testing
|
|
||||||
* @returns Array of invalid contact objects
|
|
||||||
*/
|
|
||||||
export const createInvalidContacts = (): Partial<Contact>[] => [
|
|
||||||
{},
|
|
||||||
{ did: "" },
|
|
||||||
{ did: "invalid-did" },
|
|
||||||
{ 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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create contact with specific characteristics for testing
|
|
||||||
*/
|
|
||||||
export const createContactWithMethods = (methods: ContactMethod[]): Contact =>
|
|
||||||
createStandardMockContact({ contactMethods: methods });
|
|
||||||
|
|
||||||
export const createContactWithNotes = (notes: string): Contact =>
|
|
||||||
createStandardMockContact({ notes });
|
|
||||||
|
|
||||||
export const createContactWithName = (name: string): Contact =>
|
|
||||||
createStandardMockContact({ name });
|
|
||||||
|
|
||||||
export const createContactWithDid = (did: string): Contact =>
|
|
||||||
createStandardMockContact({ did });
|
|
||||||
|
|
||||||
export const createRegisteredContact = (): Contact =>
|
|
||||||
createStandardMockContact({ registered: true });
|
|
||||||
|
|
||||||
export const createUnregisteredContact = (): Contact =>
|
|
||||||
createStandardMockContact({ registered: false });
|
|
||||||
|
|
||||||
export const createContactThatSeesMe = (): Contact =>
|
|
||||||
createStandardMockContact({ seesMe: true });
|
|
||||||
|
|
||||||
export const createContactThatDoesntSeeMe = (): Contact =>
|
|
||||||
createStandardMockContact({ seesMe: false });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock project data for testing
|
|
||||||
*/
|
|
||||||
export const createMockProject = (overrides = {}) => ({
|
|
||||||
id: `project-${Date.now()}`,
|
|
||||||
name: `Test Project ${Date.now()}`,
|
|
||||||
description: "Test project description",
|
|
||||||
status: "active",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock account data for testing
|
|
||||||
*/
|
|
||||||
export const createMockAccount = (overrides = {}) => ({
|
|
||||||
id: `account-${Date.now()}`,
|
|
||||||
name: `Test Account ${Date.now()}`,
|
|
||||||
email: "test@example.com",
|
|
||||||
balance: 100.0,
|
|
||||||
currency: "USD",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock transaction data for testing
|
|
||||||
*/
|
|
||||||
export const createMockTransaction = (overrides = {}) => ({
|
|
||||||
id: `transaction-${Date.now()}`,
|
|
||||||
amount: 50.0,
|
|
||||||
type: "credit",
|
|
||||||
description: "Test transaction",
|
|
||||||
status: "completed",
|
|
||||||
createdAt: new Date(),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock user data for testing
|
|
||||||
*/
|
|
||||||
export const createMockUser = (overrides = {}) => ({
|
|
||||||
id: `user-${Date.now()}`,
|
|
||||||
username: `testuser${Date.now()}`,
|
|
||||||
email: "test@example.com",
|
|
||||||
firstName: "Test",
|
|
||||||
lastName: "User",
|
|
||||||
isActive: true,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock settings data for testing
|
|
||||||
*/
|
|
||||||
export const createMockSettings = (overrides = {}) => ({
|
|
||||||
theme: "light",
|
|
||||||
language: "en",
|
|
||||||
notifications: true,
|
|
||||||
autoSave: true,
|
|
||||||
privacy: {
|
|
||||||
profileVisibility: "public",
|
|
||||||
dataSharing: false,
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock notification data for testing
|
|
||||||
*/
|
|
||||||
export const createMockNotification = (overrides = {}) => ({
|
|
||||||
id: `notification-${Date.now()}`,
|
|
||||||
type: "info",
|
|
||||||
title: "Test Notification",
|
|
||||||
message: "This is a test notification",
|
|
||||||
isRead: false,
|
|
||||||
createdAt: new Date(),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock error data for testing
|
|
||||||
*/
|
|
||||||
export const createMockError = (overrides = {}) => ({
|
|
||||||
code: "TEST_ERROR",
|
|
||||||
message: "Test error message",
|
|
||||||
details: "Test error details",
|
|
||||||
timestamp: new Date(),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock API response data for testing
|
|
||||||
*/
|
|
||||||
export const createMockApiResponse = (overrides = {}) => ({
|
|
||||||
success: true,
|
|
||||||
data: {},
|
|
||||||
message: "Success",
|
|
||||||
timestamp: new Date(),
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock pagination data for testing
|
|
||||||
*/
|
|
||||||
export const createMockPagination = (overrides = {}) => ({
|
|
||||||
page: 1,
|
|
||||||
limit: 10,
|
|
||||||
total: 100,
|
|
||||||
totalPages: 10,
|
|
||||||
hasNext: true,
|
|
||||||
hasPrev: false,
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { config } from "@vue/test-utils";
|
|
||||||
import { vi } from "vitest";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Setup Configuration for TimeSafari
|
|
||||||
*
|
|
||||||
* Configures the testing environment for Vue components with proper mocking
|
|
||||||
* and global test utilities. Sets up JSDOM environment for component testing.
|
|
||||||
*
|
|
||||||
* @author Matthew Raymer
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Mock global objects that might not be available in JSDOM
|
|
||||||
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
|
||||||
observe: vi.fn(),
|
|
||||||
unobserve: vi.fn(),
|
|
||||||
disconnect: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock IntersectionObserver
|
|
||||||
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
|
||||||
observe: vi.fn(),
|
|
||||||
unobserve: vi.fn(),
|
|
||||||
disconnect: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock matchMedia
|
|
||||||
Object.defineProperty(window, "matchMedia", {
|
|
||||||
writable: true,
|
|
||||||
value: vi.fn().mockImplementation((query) => ({
|
|
||||||
matches: false,
|
|
||||||
media: query,
|
|
||||||
onchange: null,
|
|
||||||
addListener: vi.fn(), // deprecated
|
|
||||||
removeListener: vi.fn(), // deprecated
|
|
||||||
addEventListener: vi.fn(),
|
|
||||||
removeEventListener: vi.fn(),
|
|
||||||
dispatchEvent: vi.fn(),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock localStorage
|
|
||||||
const localStorageMock = {
|
|
||||||
getItem: vi.fn(),
|
|
||||||
setItem: vi.fn(),
|
|
||||||
removeItem: vi.fn(),
|
|
||||||
clear: vi.fn(),
|
|
||||||
};
|
|
||||||
global.localStorage = localStorageMock;
|
|
||||||
|
|
||||||
// Mock sessionStorage
|
|
||||||
const sessionStorageMock = {
|
|
||||||
getItem: vi.fn(),
|
|
||||||
setItem: vi.fn(),
|
|
||||||
removeItem: vi.fn(),
|
|
||||||
clear: vi.fn(),
|
|
||||||
};
|
|
||||||
global.sessionStorage = sessionStorageMock;
|
|
||||||
|
|
||||||
// Configure Vue Test Utils
|
|
||||||
config.global.stubs = {
|
|
||||||
// Add any global component stubs here
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock console methods to reduce noise in tests
|
|
||||||
const originalConsole = { ...console };
|
|
||||||
beforeEach(() => {
|
|
||||||
console.warn = vi.fn();
|
|
||||||
console.error = vi.fn();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
console.warn = originalConsole.warn;
|
|
||||||
console.error = originalConsole.error;
|
|
||||||
});
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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: '<div class="entity-icon-stub">EntityIcon</div>',
|
|
||||||
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 = <T>(baseData: T) => {
|
|
||||||
return (overrides: Partial<T> = {}) => ({
|
|
||||||
...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<string, jest.Mock> = {};
|
|
||||||
|
|
||||||
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<void>;
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
@@ -1,886 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test Utilities for TimeSafari Component Testing
|
|
||||||
*
|
|
||||||
* Provides standardized test patterns, helpers, and utilities
|
|
||||||
* for consistent component testing across the application.
|
|
||||||
*
|
|
||||||
* @author Matthew Raymer
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { mount, VueWrapper } from "@vue/test-utils";
|
|
||||||
import { ComponentPublicInstance } from "vue";
|
|
||||||
import { vi } from "vitest";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standardized test setup interface
|
|
||||||
*/
|
|
||||||
export interface TestSetup {
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance> | null;
|
|
||||||
mountComponent: (props?: any) => VueWrapper<ComponentPublicInstance>;
|
|
||||||
cleanup: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standardized beforeEach pattern for all component tests
|
|
||||||
* @param component - Vue component to test
|
|
||||||
* @param defaultProps - Default props for the component
|
|
||||||
* @param globalOptions - Global options for mounting
|
|
||||||
* @returns Test setup object
|
|
||||||
*/
|
|
||||||
export const createTestSetup = (
|
|
||||||
component: any,
|
|
||||||
defaultProps = {},
|
|
||||||
globalOptions = {},
|
|
||||||
) => {
|
|
||||||
let wrapper: VueWrapper<ComponentPublicInstance> | null = null;
|
|
||||||
|
|
||||||
const mountComponent = (props = {}) => {
|
|
||||||
return mount(component, {
|
|
||||||
props: { ...defaultProps, ...props },
|
|
||||||
global: globalOptions,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
if (wrapper) {
|
|
||||||
wrapper.unmount();
|
|
||||||
wrapper = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
wrapper,
|
|
||||||
mountComponent,
|
|
||||||
cleanup,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standardized beforeEach function
|
|
||||||
* @param setup - Test setup object
|
|
||||||
*/
|
|
||||||
export const standardBeforeEach = (setup: TestSetup) => {
|
|
||||||
setup.wrapper = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standardized afterEach function
|
|
||||||
* @param setup - Test setup object
|
|
||||||
*/
|
|
||||||
export const standardAfterEach = (setup: TestSetup) => {
|
|
||||||
if (setup.wrapper) {
|
|
||||||
setup.wrapper.unmount();
|
|
||||||
setup.wrapper = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for async operations to complete
|
|
||||||
* @param ms - Milliseconds to wait
|
|
||||||
* @returns Promise that resolves after the specified time
|
|
||||||
*/
|
|
||||||
export const waitForAsync = (ms: number = 0): Promise<void> => {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for Vue to finish updating
|
|
||||||
* @param wrapper - Vue test wrapper
|
|
||||||
* @returns Promise that resolves after Vue updates
|
|
||||||
*/
|
|
||||||
export const waitForVueUpdate = async (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
) => {
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
await waitForAsync(10); // Small delay to ensure all updates are complete
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock store for testing
|
|
||||||
* @returns Mock Vuex store
|
|
||||||
*/
|
|
||||||
export const createMockStore = () => ({
|
|
||||||
state: {
|
|
||||||
user: { isRegistered: false },
|
|
||||||
contacts: [],
|
|
||||||
projects: [],
|
|
||||||
},
|
|
||||||
getters: {
|
|
||||||
isUserRegistered: (state: any) => state.user.isRegistered,
|
|
||||||
getContacts: (state: any) => state.contacts,
|
|
||||||
getProjects: (state: any) => state.projects,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
setUserRegistered: vi.fn(),
|
|
||||||
setContacts: vi.fn(),
|
|
||||||
setProjects: vi.fn(),
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
fetchContacts: vi.fn(),
|
|
||||||
fetchProjects: vi.fn(),
|
|
||||||
updateUser: vi.fn(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock router for testing
|
|
||||||
* @returns Mock Vue router
|
|
||||||
*/
|
|
||||||
export const createMockRouter = () => ({
|
|
||||||
push: vi.fn(),
|
|
||||||
replace: vi.fn(),
|
|
||||||
go: vi.fn(),
|
|
||||||
back: vi.fn(),
|
|
||||||
forward: vi.fn(),
|
|
||||||
currentRoute: {
|
|
||||||
value: {
|
|
||||||
name: "home",
|
|
||||||
path: "/",
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock service for testing
|
|
||||||
* @returns Mock service object
|
|
||||||
*/
|
|
||||||
export const createMockService = () => ({
|
|
||||||
getData: vi.fn().mockResolvedValue([]),
|
|
||||||
saveData: vi.fn().mockResolvedValue(true),
|
|
||||||
deleteData: vi.fn().mockResolvedValue(true),
|
|
||||||
updateData: vi.fn().mockResolvedValue(true),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock API client for testing
|
|
||||||
* @returns Mock API client object
|
|
||||||
*/
|
|
||||||
export const createMockApiClient = () => ({
|
|
||||||
get: vi.fn().mockResolvedValue({ data: {} }),
|
|
||||||
post: vi.fn().mockResolvedValue({ data: {} }),
|
|
||||||
put: vi.fn().mockResolvedValue({ data: {} }),
|
|
||||||
delete: vi.fn().mockResolvedValue({ data: {} }),
|
|
||||||
patch: vi.fn().mockResolvedValue({ data: {} }),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock notification service for testing
|
|
||||||
* @returns Mock notification service object
|
|
||||||
*/
|
|
||||||
export const createMockNotificationService = () => ({
|
|
||||||
show: vi.fn().mockResolvedValue(true),
|
|
||||||
hide: vi.fn().mockResolvedValue(true),
|
|
||||||
success: vi.fn().mockResolvedValue(true),
|
|
||||||
error: vi.fn().mockResolvedValue(true),
|
|
||||||
warning: vi.fn().mockResolvedValue(true),
|
|
||||||
info: vi.fn().mockResolvedValue(true),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock storage service for testing
|
|
||||||
* @returns Mock storage service object
|
|
||||||
*/
|
|
||||||
export const createMockStorageService = () => ({
|
|
||||||
getItem: vi.fn().mockReturnValue(null),
|
|
||||||
setItem: vi.fn().mockReturnValue(true),
|
|
||||||
removeItem: vi.fn().mockReturnValue(true),
|
|
||||||
clear: vi.fn().mockReturnValue(true),
|
|
||||||
key: vi.fn().mockReturnValue(null),
|
|
||||||
length: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock authentication service for testing
|
|
||||||
* @returns Mock authentication service object
|
|
||||||
*/
|
|
||||||
export const createMockAuthService = () => ({
|
|
||||||
login: vi.fn().mockResolvedValue({ user: {}, token: "mock-token" }),
|
|
||||||
logout: vi.fn().mockResolvedValue(true),
|
|
||||||
register: vi.fn().mockResolvedValue({ user: {}, token: "mock-token" }),
|
|
||||||
isAuthenticated: vi.fn().mockReturnValue(true),
|
|
||||||
getCurrentUser: vi.fn().mockReturnValue({ id: 1, name: "Test User" }),
|
|
||||||
refreshToken: vi.fn().mockResolvedValue("new-mock-token"),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock database service for testing
|
|
||||||
* @returns Mock database service object
|
|
||||||
*/
|
|
||||||
export const createMockDatabaseService = () => ({
|
|
||||||
query: vi.fn().mockResolvedValue([]),
|
|
||||||
execute: vi.fn().mockResolvedValue({ affectedRows: 1 }),
|
|
||||||
transaction: vi.fn().mockImplementation(async (callback) => {
|
|
||||||
return await callback({
|
|
||||||
query: vi.fn().mockResolvedValue([]),
|
|
||||||
execute: vi.fn().mockResolvedValue({ affectedRows: 1 }),
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
close: vi.fn().mockResolvedValue(true),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mock file system service for testing
|
|
||||||
* @returns Mock file system service object
|
|
||||||
*/
|
|
||||||
export const createMockFileSystemService = () => ({
|
|
||||||
readFile: vi.fn().mockResolvedValue("file content"),
|
|
||||||
writeFile: vi.fn().mockResolvedValue(true),
|
|
||||||
deleteFile: vi.fn().mockResolvedValue(true),
|
|
||||||
exists: vi.fn().mockResolvedValue(true),
|
|
||||||
createDirectory: vi.fn().mockResolvedValue(true),
|
|
||||||
listFiles: vi.fn().mockResolvedValue(["file1.txt", "file2.txt"]),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performance testing utilities
|
|
||||||
*/
|
|
||||||
export const performanceUtils = {
|
|
||||||
/**
|
|
||||||
* Measure execution time of a function
|
|
||||||
* @param fn - Function to measure
|
|
||||||
* @returns Object with timing information
|
|
||||||
*/
|
|
||||||
measureTime: async (fn: () => any) => {
|
|
||||||
const start = performance.now();
|
|
||||||
const result = await fn();
|
|
||||||
const end = performance.now();
|
|
||||||
return {
|
|
||||||
result,
|
|
||||||
duration: end - start,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if performance is within acceptable limits
|
|
||||||
* @param duration - Duration in milliseconds
|
|
||||||
* @param threshold - Maximum acceptable duration
|
|
||||||
* @returns Boolean indicating if performance is acceptable
|
|
||||||
*/
|
|
||||||
isWithinThreshold: (duration: number, threshold: number = 200) => {
|
|
||||||
return duration < threshold;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Accessibility testing utilities
|
|
||||||
*/
|
|
||||||
export const accessibilityUtils = {
|
|
||||||
/**
|
|
||||||
* Check if element has required ARIA attributes
|
|
||||||
* @param element - DOM element to check
|
|
||||||
* @param requiredAttributes - Array of required ARIA attributes
|
|
||||||
* @returns Boolean indicating if all required attributes are present
|
|
||||||
*/
|
|
||||||
hasRequiredAriaAttributes: (element: any, requiredAttributes: string[]) => {
|
|
||||||
return requiredAttributes.every(
|
|
||||||
(attr) => element.attributes(attr) !== undefined,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if element is keyboard accessible
|
|
||||||
* @param element - DOM element to check
|
|
||||||
* @returns Boolean indicating if element is keyboard accessible
|
|
||||||
*/
|
|
||||||
isKeyboardAccessible: (element: any) => {
|
|
||||||
const tabindex = element.attributes("tabindex");
|
|
||||||
const role = element.attributes("role");
|
|
||||||
return tabindex !== undefined || role === "button" || role === "link";
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error testing utilities
|
|
||||||
*/
|
|
||||||
export const errorUtils = {
|
|
||||||
/**
|
|
||||||
* Test component with various invalid prop combinations
|
|
||||||
* @param mountComponent - Function to mount component
|
|
||||||
* @param invalidProps - Array of invalid prop combinations
|
|
||||||
* @returns Array of test results
|
|
||||||
*/
|
|
||||||
testInvalidProps: async (
|
|
||||||
mountComponent: (props?: any) => VueWrapper<ComponentPublicInstance>,
|
|
||||||
invalidProps: any[],
|
|
||||||
) => {
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (const props of invalidProps) {
|
|
||||||
try {
|
|
||||||
const wrapper = mountComponent(props);
|
|
||||||
results.push({
|
|
||||||
props,
|
|
||||||
success: true,
|
|
||||||
error: null,
|
|
||||||
wrapper: wrapper.exists(),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
props,
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
wrapper: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component lifecycle testing utilities
|
|
||||||
*/
|
|
||||||
export const lifecycleUtils = {
|
|
||||||
/**
|
|
||||||
* Test component mounting lifecycle
|
|
||||||
*/
|
|
||||||
testMounting: async (component: any, props = {}) => {
|
|
||||||
const wrapper = mount(component, { props });
|
|
||||||
const vm = wrapper.vm as any;
|
|
||||||
|
|
||||||
// Test mounted hook
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
|
|
||||||
// Test data initialization
|
|
||||||
expect(vm).toBeDefined();
|
|
||||||
|
|
||||||
return wrapper;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test component unmounting lifecycle
|
|
||||||
*/
|
|
||||||
testUnmounting: async (wrapper: VueWrapper<ComponentPublicInstance>) => {
|
|
||||||
// Test beforeUnmount hook
|
|
||||||
await wrapper.unmount();
|
|
||||||
|
|
||||||
// Verify component is destroyed
|
|
||||||
expect(wrapper.exists()).toBe(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test component prop updates
|
|
||||||
*/
|
|
||||||
testPropUpdates: async (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
propUpdates: any[],
|
|
||||||
) => {
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (const update of propUpdates) {
|
|
||||||
await wrapper.setProps(update.props);
|
|
||||||
await waitForVueUpdate(wrapper);
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
update,
|
|
||||||
success: true,
|
|
||||||
props: wrapper.props(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed property testing utilities
|
|
||||||
*/
|
|
||||||
export const computedUtils = {
|
|
||||||
/**
|
|
||||||
* Test computed property values
|
|
||||||
*/
|
|
||||||
testComputedProperty: (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
propertyName: string,
|
|
||||||
expectedValue: any,
|
|
||||||
) => {
|
|
||||||
const vm = wrapper.vm as any;
|
|
||||||
expect(vm[propertyName]).toBe(expectedValue);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test computed property dependencies
|
|
||||||
*/
|
|
||||||
testComputedDependencies: async (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
propertyName: string,
|
|
||||||
dependencyUpdates: any[],
|
|
||||||
) => {
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (const update of dependencyUpdates) {
|
|
||||||
await wrapper.setProps(update.props);
|
|
||||||
await waitForVueUpdate(wrapper);
|
|
||||||
|
|
||||||
const vm = wrapper.vm as any;
|
|
||||||
results.push({
|
|
||||||
update,
|
|
||||||
computedValue: vm[propertyName],
|
|
||||||
expectedValue: update.expectedValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test computed property caching
|
|
||||||
*/
|
|
||||||
testComputedCaching: (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
propertyName: string,
|
|
||||||
) => {
|
|
||||||
const vm = wrapper.vm as any;
|
|
||||||
const firstCall = vm[propertyName];
|
|
||||||
const secondCall = vm[propertyName];
|
|
||||||
|
|
||||||
// Computed properties should return the same value without recalculation
|
|
||||||
expect(firstCall).toBe(secondCall);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Watcher testing utilities
|
|
||||||
*/
|
|
||||||
export const watcherUtils = {
|
|
||||||
/**
|
|
||||||
* Test watcher triggers
|
|
||||||
*/
|
|
||||||
testWatcherTrigger: async (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
propertyName: string,
|
|
||||||
newValue: any,
|
|
||||||
) => {
|
|
||||||
const vm = wrapper.vm as any;
|
|
||||||
const originalValue = vm[propertyName];
|
|
||||||
|
|
||||||
// Use setProps instead of direct property assignment for Vue 3
|
|
||||||
await wrapper.setProps({ [propertyName]: newValue });
|
|
||||||
await waitForVueUpdate(wrapper);
|
|
||||||
|
|
||||||
return {
|
|
||||||
originalValue,
|
|
||||||
newValue,
|
|
||||||
triggered: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test watcher cleanup
|
|
||||||
*/
|
|
||||||
testWatcherCleanup: async (wrapper: VueWrapper<ComponentPublicInstance>) => {
|
|
||||||
const vm = wrapper.vm as any;
|
|
||||||
|
|
||||||
// Store watcher references
|
|
||||||
const watchers = vm.$options?.watch || {};
|
|
||||||
|
|
||||||
// Unmount component
|
|
||||||
await wrapper.unmount();
|
|
||||||
|
|
||||||
return {
|
|
||||||
watchersCount: Object.keys(watchers).length,
|
|
||||||
unmounted: !wrapper.exists(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test deep watchers
|
|
||||||
*/
|
|
||||||
testDeepWatcher: async (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
propertyPath: string,
|
|
||||||
newValue: any,
|
|
||||||
) => {
|
|
||||||
// For Vue 3, we'll test prop changes instead of direct property assignment
|
|
||||||
await wrapper.setProps({ [propertyPath]: newValue });
|
|
||||||
await waitForVueUpdate(wrapper);
|
|
||||||
|
|
||||||
return {
|
|
||||||
propertyPath,
|
|
||||||
newValue,
|
|
||||||
updated: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event modifier testing utilities
|
|
||||||
*/
|
|
||||||
export const eventModifierUtils = {
|
|
||||||
/**
|
|
||||||
* Test .prevent modifier
|
|
||||||
*/
|
|
||||||
testPreventModifier: async (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
selector: string,
|
|
||||||
) => {
|
|
||||||
const element = wrapper.find(selector);
|
|
||||||
|
|
||||||
await element.trigger("click", { preventDefault: () => {} });
|
|
||||||
|
|
||||||
return {
|
|
||||||
eventTriggered: true,
|
|
||||||
preventDefaultCalled: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test .stop modifier
|
|
||||||
*/
|
|
||||||
testStopModifier: async (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
selector: string,
|
|
||||||
) => {
|
|
||||||
const element = wrapper.find(selector);
|
|
||||||
|
|
||||||
await element.trigger("click", { stopPropagation: () => {} });
|
|
||||||
|
|
||||||
return {
|
|
||||||
eventTriggered: true,
|
|
||||||
stopPropagationCalled: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test .once modifier
|
|
||||||
*/
|
|
||||||
testOnceModifier: async (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
selector: string,
|
|
||||||
) => {
|
|
||||||
const element = wrapper.find(selector);
|
|
||||||
|
|
||||||
// First click
|
|
||||||
await element.trigger("click");
|
|
||||||
const firstEmit = wrapper.emitted();
|
|
||||||
|
|
||||||
// Second click
|
|
||||||
await element.trigger("click");
|
|
||||||
const secondEmit = wrapper.emitted();
|
|
||||||
|
|
||||||
return {
|
|
||||||
firstClickEmitted: Object.keys(firstEmit).length > 0,
|
|
||||||
secondClickEmitted:
|
|
||||||
Object.keys(secondEmit).length === Object.keys(firstEmit).length,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test .self modifier
|
|
||||||
*/
|
|
||||||
testSelfModifier: async (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
selector: string,
|
|
||||||
) => {
|
|
||||||
const element = wrapper.find(selector);
|
|
||||||
|
|
||||||
// Click on the element itself
|
|
||||||
await element.trigger("click");
|
|
||||||
const selfClickEmitted = wrapper.emitted();
|
|
||||||
|
|
||||||
// Click on a child element
|
|
||||||
const child = element.find("*");
|
|
||||||
if (child.exists()) {
|
|
||||||
await child.trigger("click");
|
|
||||||
}
|
|
||||||
const secondEmit = wrapper.emitted();
|
|
||||||
|
|
||||||
return {
|
|
||||||
selfClickEmitted: Object.keys(selfClickEmitted).length > 0,
|
|
||||||
childClickEmitted:
|
|
||||||
Object.keys(secondEmit).length === Object.keys(selfClickEmitted).length,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enhanced assertion utilities
|
|
||||||
*/
|
|
||||||
export const assertionUtils = {
|
|
||||||
/**
|
|
||||||
* Assert component has required props
|
|
||||||
*/
|
|
||||||
assertRequiredProps: (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
requiredProps: string[],
|
|
||||||
) => {
|
|
||||||
const vm = wrapper.vm as any;
|
|
||||||
requiredProps.forEach((prop) => {
|
|
||||||
expect(vm[prop]).toBeDefined();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component emits expected events
|
|
||||||
*/
|
|
||||||
assertEmitsEvents: (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
expectedEvents: string[],
|
|
||||||
) => {
|
|
||||||
const emitted = wrapper.emitted();
|
|
||||||
expectedEvents.forEach((event) => {
|
|
||||||
expect(emitted[event]).toBeDefined();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component has correct CSS classes
|
|
||||||
*/
|
|
||||||
assertHasClasses: (element: any, expectedClasses: string[]) => {
|
|
||||||
expectedClasses.forEach((className) => {
|
|
||||||
expect(element.classes()).toContain(className);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component has correct attributes
|
|
||||||
*/
|
|
||||||
assertHasAttributes: (
|
|
||||||
element: any,
|
|
||||||
expectedAttributes: Record<string, string>,
|
|
||||||
) => {
|
|
||||||
Object.entries(expectedAttributes).forEach(([attr, value]) => {
|
|
||||||
expect(element.attributes(attr)).toBe(value);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component is accessible
|
|
||||||
*/
|
|
||||||
assertIsAccessible: (element: any) => {
|
|
||||||
const tabindex = element.attributes("tabindex");
|
|
||||||
const role = element.attributes("role");
|
|
||||||
const ariaLabel = element.attributes("aria-label");
|
|
||||||
|
|
||||||
expect(
|
|
||||||
tabindex !== undefined || role !== undefined || ariaLabel !== undefined,
|
|
||||||
).toBe(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component is keyboard navigable
|
|
||||||
*/
|
|
||||||
assertIsKeyboardNavigable: (element: any) => {
|
|
||||||
const tabindex = element.attributes("tabindex");
|
|
||||||
expect(
|
|
||||||
tabindex !== undefined || element.attributes("role") === "button",
|
|
||||||
).toBe(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component has proper ARIA attributes
|
|
||||||
*/
|
|
||||||
assertHasAriaAttributes: (element: any, requiredAria: string[]) => {
|
|
||||||
requiredAria.forEach((attr) => {
|
|
||||||
expect(element.attributes(attr)).toBeDefined();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component renders correctly with props
|
|
||||||
*/
|
|
||||||
assertRendersWithProps: (component: any, props: any) => {
|
|
||||||
const wrapper = mount(component, { props });
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
return wrapper;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component handles prop changes correctly
|
|
||||||
*/
|
|
||||||
assertHandlesPropChanges: async (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
propChanges: any[],
|
|
||||||
) => {
|
|
||||||
for (const change of propChanges) {
|
|
||||||
await wrapper.setProps(change.props);
|
|
||||||
await waitForVueUpdate(wrapper);
|
|
||||||
|
|
||||||
if (change.expected) {
|
|
||||||
expect(wrapper.html()).toContain(change.expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component performance is acceptable
|
|
||||||
*/
|
|
||||||
assertPerformance: async (fn: () => any, maxDuration: number = 200) => {
|
|
||||||
const start = performance.now();
|
|
||||||
await fn();
|
|
||||||
const duration = performance.now() - start;
|
|
||||||
|
|
||||||
expect(duration).toBeLessThan(maxDuration);
|
|
||||||
return duration;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component doesn't cause memory leaks
|
|
||||||
*/
|
|
||||||
assertNoMemoryLeaks: async (component: any, props: any = {}) => {
|
|
||||||
// Memory testing is not reliable in JSDOM environment
|
|
||||||
// Instead, test that component can be mounted and unmounted repeatedly
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
const wrapper = mount(component, { props });
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
await wrapper.unmount();
|
|
||||||
expect(wrapper.exists()).toBe(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component error handling
|
|
||||||
*/
|
|
||||||
assertErrorHandling: async (component: any, invalidProps: any[]) => {
|
|
||||||
for (const props of invalidProps) {
|
|
||||||
try {
|
|
||||||
const wrapper = mount(component, { props });
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
} catch (error) {
|
|
||||||
// Component should handle invalid props gracefully
|
|
||||||
expect(error).toBeDefined();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert component accessibility compliance
|
|
||||||
*/
|
|
||||||
assertAccessibilityCompliance: (
|
|
||||||
wrapper: VueWrapper<ComponentPublicInstance>,
|
|
||||||
) => {
|
|
||||||
const html = wrapper.html();
|
|
||||||
|
|
||||||
// Check for semantic HTML elements
|
|
||||||
expect(html).toMatch(
|
|
||||||
/<(button|input|select|textarea|a|nav|main|section|article|header|footer)/,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check for ARIA attributes
|
|
||||||
expect(html).toMatch(/aria-|role=/);
|
|
||||||
|
|
||||||
// Check for proper heading structure
|
|
||||||
const headings = html.match(/<h[1-6]/g);
|
|
||||||
if (headings) {
|
|
||||||
expect(headings.length).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component testing utilities
|
|
||||||
*/
|
|
||||||
export const componentUtils = {
|
|
||||||
/**
|
|
||||||
* Test component with different prop combinations
|
|
||||||
*/
|
|
||||||
testPropCombinations: async (component: any, propCombinations: any[]) => {
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (const props of propCombinations) {
|
|
||||||
try {
|
|
||||||
const wrapper = mount(component, { props });
|
|
||||||
results.push({
|
|
||||||
props,
|
|
||||||
success: true,
|
|
||||||
rendered: wrapper.exists(),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
props,
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test component with different screen sizes
|
|
||||||
*/
|
|
||||||
testResponsiveBehavior: async (component: any, props: any = {}) => {
|
|
||||||
const screenSizes = [
|
|
||||||
{ width: 320, height: 568 }, // Mobile
|
|
||||||
{ width: 768, height: 1024 }, // Tablet
|
|
||||||
{ width: 1024, height: 768 }, // Desktop
|
|
||||||
{ width: 1920, height: 1080 }, // Large Desktop
|
|
||||||
];
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (const size of screenSizes) {
|
|
||||||
Object.defineProperty(window, "innerWidth", { value: size.width });
|
|
||||||
Object.defineProperty(window, "innerHeight", { value: size.height });
|
|
||||||
|
|
||||||
const wrapper = mount(component, { props });
|
|
||||||
results.push({
|
|
||||||
size,
|
|
||||||
rendered: wrapper.exists(),
|
|
||||||
html: wrapper.html(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test component with different themes
|
|
||||||
*/
|
|
||||||
testThemeBehavior: async (component: any, props: any = {}) => {
|
|
||||||
const themes = ["light", "dark", "auto"];
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (const theme of themes) {
|
|
||||||
const wrapper = mount(component, {
|
|
||||||
props,
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
theme,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
theme,
|
|
||||||
rendered: wrapper.exists(),
|
|
||||||
classes: wrapper.classes(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test component with different languages
|
|
||||||
*/
|
|
||||||
testInternationalization: async (component: any, props: any = {}) => {
|
|
||||||
const languages = ["en", "es", "fr", "de"];
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (const lang of languages) {
|
|
||||||
const wrapper = mount(component, {
|
|
||||||
props,
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
locale: lang,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
language: lang,
|
|
||||||
rendered: wrapper.exists(),
|
|
||||||
text: wrapper.text(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -831,26 +831,3 @@ export default class DIDView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
z-index: 50;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
55
src/vite.config.utils.js
Normal file
55
src/vite.config.utils.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as path from "path";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
export async function loadAppConfig() {
|
||||||
|
const packageJson = await loadPackageJson();
|
||||||
|
const appName = process.env.TIME_SAFARI_APP_TITLE || packageJson.name;
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
return {
|
||||||
|
pwaConfig: {
|
||||||
|
manifest: {
|
||||||
|
name: appName,
|
||||||
|
short_name: appName,
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-maskable-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "maskable",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-maskable-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "maskable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aliasConfig: {
|
||||||
|
"@": path.resolve(path.dirname(__dirname), "src"),
|
||||||
|
buffer: path.resolve(path.dirname(__dirname), "node_modules", "buffer"),
|
||||||
|
"dexie-export-import/dist/import":
|
||||||
|
"dexie-export-import/dist/import/index.js",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPackageJson() {
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const packageJsonPath = path.resolve(path.dirname(__dirname), "package.json");
|
||||||
|
const packageJsonData = await fs.readFile(packageJsonPath, "utf-8");
|
||||||
|
return JSON.parse(packageJsonData);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
module.exports = {
|
||||||
content: ["./src/**/*.vue"],
|
content: ["./src/**/*.vue"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
@@ -23,11 +23,10 @@ test('New offers for another user', async ({ page }) => {
|
|||||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(autoCreatedDid + ', A Friend');
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(autoCreatedDid + ', A Friend');
|
||||||
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
|
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
|
||||||
await page.locator('button > svg.fa-plus').click();
|
await page.locator('button > svg.fa-plus').click();
|
||||||
await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible(); // wait for info alert to be visible…
|
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
||||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // …and dismiss it
|
await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible();
|
||||||
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
|
||||||
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
||||||
await page.locator('div[role="alert"] button:text-is("No")').click(); // Dismiss register prompt
|
|
||||||
await page.locator('div[role="alert"] button:text-is("No, Not Now")').click(); // Dismiss export data prompt
|
|
||||||
|
|
||||||
// show buttons to make offers directly to people
|
// show buttons to make offers directly to people
|
||||||
await page.getByRole('button').filter({ hasText: /See Actions/i }).click();
|
await page.getByRole('button').filter({ hasText: /See Actions/i }).click();
|
||||||
|
|||||||
@@ -158,10 +158,10 @@ export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
|
|||||||
.fill(`${newDid}, ${contactName}`);
|
.fill(`${newDid}, ${contactName}`);
|
||||||
await page.locator("button > svg.fa-plus").click();
|
await page.locator("button > svg.fa-plus").click();
|
||||||
// register them
|
// register them
|
||||||
await page.locator('div[role="alert"] button:text-is("Yes")').click();
|
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
||||||
// wait for it to disappear because the next steps may depend on alerts being gone
|
// wait for it to disappear because the next steps may depend on alerts being gone
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('div[role="alert"] button:text-is("Yes")')
|
page.locator('div[role="alert"] button:has-text("Yes")')
|
||||||
).toBeHidden();
|
).toBeHidden();
|
||||||
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
|
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { defineConfig } from 'vitest/config'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import { resolve } from 'path'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vitest Configuration for TimeSafari
|
|
||||||
*
|
|
||||||
* Configures testing environment for Vue components with JSDOM support.
|
|
||||||
* Enables testing of Vue-facing-decorator components with proper TypeScript support.
|
|
||||||
* Excludes Playwright tests which use a different testing framework.
|
|
||||||
*
|
|
||||||
* @author Matthew Raymer
|
|
||||||
*/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()],
|
|
||||||
test: {
|
|
||||||
environment: 'jsdom',
|
|
||||||
globals: true,
|
|
||||||
setupFiles: ['./src/test/setup.ts'],
|
|
||||||
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
||||||
exclude: [
|
|
||||||
'node_modules',
|
|
||||||
'dist',
|
|
||||||
'.idea',
|
|
||||||
'.git',
|
|
||||||
'.cache',
|
|
||||||
'test-playwright/**/*',
|
|
||||||
'test-scripts/**/*',
|
|
||||||
'test-results/**/*',
|
|
||||||
'test-playwright-results/**/*'
|
|
||||||
],
|
|
||||||
coverage: {
|
|
||||||
provider: 'v8',
|
|
||||||
reporter: ['text', 'json', 'html'],
|
|
||||||
exclude: [
|
|
||||||
'node_modules/',
|
|
||||||
'src/test/',
|
|
||||||
'**/*.d.ts',
|
|
||||||
'**/*.config.*',
|
|
||||||
'**/coverage/**',
|
|
||||||
'test-playwright/**/*'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': resolve(__dirname, './src')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user