+
+
+ {{ metric.label }}
-
+
@if (metric.badge) {
: metric.badge.severity === 'warning'
? 'bg-amber-500 text-white'
: 'bg-red-500 text-white'
- ">
+ "
+ data-testid="metric-badge"
+ [attr.data-badge-severity]="metric.badge.severity">
{{ metric.badge.label }}
} @else {
- {{ metric.value }}
+ {{ metric.value }}
}
diff --git a/apps/lfx-pcc/tsconfig.e2e.json b/apps/lfx-pcc/tsconfig.e2e.json
new file mode 100644
index 00000000..44377a14
--- /dev/null
+++ b/apps/lfx-pcc/tsconfig.e2e.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/e2e",
+ "module": "commonjs",
+ "target": "es2021",
+ "types": ["node"],
+ "sourceMap": false,
+ "declaration": false
+ },
+ "include": ["playwright.config.ts", "e2e/**/*.ts"]
+}
diff --git a/docs/architecture/testing/README.md b/docs/architecture/testing/README.md
index 446f005a..a0dec4d9 100644
--- a/docs/architecture/testing/README.md
+++ b/docs/architecture/testing/README.md
@@ -6,51 +6,45 @@ The testing architecture is designed to ensure code quality and reliability acro
## ๐ Testing Strategy
-### Testing Pyramid
+### Testing Approach
-- **Unit Tests**: Component and service testing
-- **Integration Tests**: API and service integration
-- **End-to-End Tests**: Full application workflows
+- **End-to-End Tests**: Full application workflows with comprehensive coverage (85+ tests)
+- **Dual Architecture**: Content-based tests for user experience + Structural tests for technical validation
### Key Principles
-- **Test-Driven Development**: Write tests before implementation
+- **Dual Test Architecture**: Content-based + Structural tests for maximum reliability
+- **Data-TestID Strategy**: Robust element targeting that survives UI changes
+- **Responsive Testing**: Multi-viewport validation (mobile, tablet, desktop)
+- **Framework-Aware Testing**: Angular signals, components, and architecture validation
- **Fast Feedback**: Quick test execution for development
- **Reliable Tests**: Consistent and deterministic test results
- **Comprehensive Coverage**: Critical paths and edge cases covered
## ๐ Documentation Sections
-### [Unit Testing](./unit-testing.md)
-
-Learn about component testing, service testing, and testing utilities for Angular applications.
-
-### [Integration Testing](./integration-testing.md)
-
-Understand API testing, database testing, and service integration patterns.
-
### [E2E Testing](./e2e-testing.md)
-Discover end-to-end testing strategies, user workflows, and browser automation.
+Comprehensive end-to-end testing with dual architecture approach, covering user workflows, component validation, and browser automation.
-## ๐ Testing Tools
+### [Testing Best Practices](./testing-best-practices.md)
-### Frontend Testing
+Complete guide to testing patterns, data-testid conventions, responsive testing, and maintenance strategies.
-- **Jest**: Unit testing framework
-- **Angular Testing Utilities**: Component and service testing
-- **Testing Library**: User-centric testing approaches
+## ๐ Testing Tools
-### Backend Testing
+### Primary Testing Framework
-- **Jest**: Node.js testing framework
-- **Supertest**: HTTP API testing
-- **Test Containers**: Database testing
+- **Playwright**: Modern browser automation framework
+- **Multi-browser Support**: Chromium, Firefox, Mobile Chrome
+- **Data-TestID Architecture**: Robust element targeting that survives UI changes
+- **Dual Testing Strategy**: Content-based + Structural tests for comprehensive coverage
-### E2E Testing
+### Supporting Tools
-- **Playwright**: Modern browser automation
-- **Cypress**: Alternative E2E framework
+- **Auth0 Integration**: Global authentication setup for testing
+- **Responsive Testing**: Multi-viewport validation
+- **Angular Integration**: Signals, components, and framework-specific validation
## ๐ Quick Links
diff --git a/docs/architecture/testing/e2e-testing.md b/docs/architecture/testing/e2e-testing.md
index de0a3ba4..8c8fc6d2 100644
--- a/docs/architecture/testing/e2e-testing.md
+++ b/docs/architecture/testing/e2e-testing.md
@@ -1,605 +1,459 @@
-# End-to-End Testing
+# End-to-End Testing Architecture
-## ๐ญ E2E Testing Strategy
+## ๐ Overview
-End-to-end tests verify complete user workflows from browser interaction to backend processing, ensuring the entire application works as expected from a user's perspective.
+Our E2E testing strategy employs a **dual architecture approach** combining content-based and structural tests to ensure comprehensive, maintainable, and reliable test coverage across the LFX Projects Self-Service application.
-## ๐ Testing Tools
+## ๐ฏ Dual Testing Architecture
-### Primary E2E Framework
+### Content-Based Tests (Original)
-#### Playwright (Recommended)
+- **Purpose**: Validate user experience and visible content
+- **Target**: Text content, user interactions, workflows
+- **Best For**: Acceptance testing, user journey validation
+- **Examples**: `homepage.spec.ts`, `project-dashboard.spec.ts`
-- Cross-browser testing (Chromium, Firefox, Safari)
-- Modern async/await API
-- Built-in waiting strategies
-- Parallel test execution
-- Great developer experience
+### Structural Tests (Robust)
-#### Alternative: Cypress
+- **Purpose**: Validate component architecture and framework integration
+- **Target**: Component structure, Angular signals, data attributes
+- **Best For**: Technical validation, UI library independence
+- **Examples**: `homepage-robust.spec.ts`, `project-dashboard-robust.spec.ts`
-- Developer-friendly debugging
-- Time-travel debugging
-- Real-time browser preview
-- Chrome-based testing
+## ๐ Current Test Coverage
-## ๐ง Playwright Configuration
+```text
+Total E2E Tests: 85+ (All Passing)
+โโโ Homepage Tests: 33 tests
+โ โโโ homepage.spec.ts: 11 content-based tests
+โ โโโ homepage-robust.spec.ts: 22 structural tests
+โโโ Project Dashboard Tests: 52 tests
+ โโโ project-dashboard.spec.ts: 29 content-based tests
+ โโโ project-dashboard-robust.spec.ts: 23 structural tests
+```
+
+## ๐ Technical Stack
+
+### Primary Framework: Playwright
+
+- **Cross-browser support**: Chromium, Firefox, Mobile Chrome
+- **Modern async/await API** with built-in waiting strategies
+- **Parallel execution** with worker configuration
+- **Authentication state management** with global setup
-### Setup Configuration
+### Browser Configuration
```typescript
-// playwright.config.ts
-import { defineConfig, devices } from '@playwright/test';
-
-export default defineConfig({
- testDir: './e2e',
- fullyParallel: true,
- forbidOnly: !!process.env['CI'],
- retries: process.env['CI'] ? 2 : 0,
- workers: process.env['CI'] ? 1 : undefined,
- reporter: 'html',
+// playwright.config.ts - Mobile Chrome Configuration
+{
+ name: 'Mobile Chrome',
use: {
- baseURL: 'http://localhost:4000',
- trace: 'on-first-retry',
- video: 'retain-on-failure',
- screenshot: 'only-on-failure',
+ ...devices['Pixel 5'],
+ storageState: 'playwright/.auth/user.json',
},
-
- projects: [
- {
- name: 'chromium',
- use: { ...devices['Desktop Chrome'] },
- },
- {
- name: 'firefox',
- use: { ...devices['Desktop Firefox'] },
- },
- {
- name: 'webkit',
- use: { ...devices['Desktop Safari'] },
- },
- {
- name: 'mobile-chrome',
- use: { ...devices['Pixel 5'] },
- },
- ],
-
- webServer: {
- command: 'yarn start',
- url: 'http://localhost:4000',
- reuseExistingServer: !process.env['CI'],
- timeout: 120000,
- },
-});
+ // Single worker to prevent resource contention
+ workers: 1,
+}
```
-## ๐ฏ Core User Workflows
+## ๐ง Data-TestID Architecture
-### Authentication Flow
+### Implementation Strategy
-```typescript
-// e2e/auth.spec.ts
-import { test, expect } from '@playwright/test';
+#### 1. Component-Level Attributes
-test.describe('Authentication', () => {
- test('should redirect unauthenticated users to Auth0', async ({ page }) => {
- await page.goto('/');
+```html
+
+
+```
- // Should redirect to Auth0
- await expect(page).toHaveURL(/auth0\.com/);
+#### 2. Dynamic Attributes for State
- // Should see Auth0 login form
- await expect(page.locator('[data-testid="login-form"]')).toBeVisible();
- });
+```html
+
+
+```
- test('should complete login flow', async ({ page }) => {
- await page.goto('/');
+#### 3. Nested Component Structure
+
+```html
+
+
+
+
+
+ {{ metric.label }}
+
+
+ {{ metric.value }}
+
+
+
+```
- // Wait for Auth0 redirect
- await page.waitForURL(/auth0\.com/);
+### Naming Conventions
- // Fill login form
- await page.fill('[data-testid="email-input"]', 'test@example.com');
- await page.fill('[data-testid="password-input"]', 'test-password');
- await page.click('[data-testid="login-button"]');
+1. **Section-level**: `data-testid="hero-section"`
+2. **Component-level**: `data-testid="project-card"`
+3. **Element-level**: `data-testid="project-title"`
+4. **Container-level**: `data-testid="metrics-cards-container"`
+5. **Dynamic identification**: `[attr.data-project-slug]="project.slug"`
- // Should redirect back to application
- await page.waitForURL('http://localhost:4000/');
+## ๐งช Test Patterns and Examples
- // Should see authenticated state
- await expect(page.locator('[data-testid="user-avatar"]')).toBeVisible();
- await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
- });
+### 1. Structural Component Validation
- test('should handle logout', async ({ page, context }) => {
- // Assume authenticated state
- await context.addCookies([
- {
- name: 'appSession',
- value: 'mock-session-token',
- domain: 'localhost',
- path: '/',
- },
- ]);
-
- await page.goto('/');
-
- // Click user menu
- await page.click('[data-testid="user-avatar"]');
- await page.click('[data-testid="logout-button"]');
-
- // Should redirect to logout and back to Auth0
- await expect(page).toHaveURL(/auth0\.com/);
- });
+```typescript
+test('should use lfx-card components consistently', async ({ page }) => {
+ const testCards = [
+ page.locator('[data-testid="total-members-card"]'),
+ page.locator('[data-testid="project-health-card"]'),
+ page.locator('[data-testid="quick-actions-card"]'),
+ ];
+
+ for (const card of testCards) {
+ await expect(card).toBeVisible();
+ // Validate component architecture
+ const tagName = await card.evaluate((el) => el.tagName.toLowerCase());
+ expect(tagName).toBe('lfx-card');
+ }
});
```
-### Project Navigation
+### 2. Angular Signals Integration Testing
```typescript
-// e2e/navigation.spec.ts
-import { test, expect } from '@playwright/test';
-
-test.describe('Project Navigation', () => {
- test.beforeEach(async ({ page, context }) => {
- // Mock authenticated state
- await context.addCookies([
- {
- name: 'appSession',
- value: 'mock-session-token',
- domain: 'localhost',
- path: '/',
- },
- ]);
- });
+test('should properly integrate Angular signals and computed values', async ({ page }) => {
+ // Wait for Angular to initialize and signals to resolve
+ await page.waitForLoadState('networkidle');
- test('should navigate from home to project page', async ({ page }) => {
- await page.goto('/');
+ // Check that percentage values are rendered (indicates successful signal integration)
+ const activityScore = page.locator('[data-testid="activity-score-indicator"] span').filter({ hasText: /%$/ });
+ await expect(activityScore).toBeVisible();
- // Wait for projects to load
- await expect(page.locator('[data-testid="project-card"]').first()).toBeVisible();
-
- // Click on first project
- const firstProject = page.locator('[data-testid="project-card"]').first();
- const projectTitle = await firstProject.locator('h3').textContent();
- await firstProject.click();
-
- // Should navigate to project page
- await expect(page).toHaveURL(/\/project\/[^\/]+$/);
+ const meetingCompletion = page.locator('[data-testid="meeting-completion-indicator"] span').filter({ hasText: /%$/ });
+ await expect(meetingCompletion).toBeVisible();
+});
+```
- // Should show project details
- await expect(page.locator('[data-testid="project-title"]')).toHaveText(projectTitle || '');
- await expect(page.locator('[data-testid="project-navigation"]')).toBeVisible();
- });
+### 3. Responsive Design Testing
- test('should navigate between project tabs', async ({ page }) => {
- await page.goto('/project/kubernetes');
+```typescript
+test('should display header elements correctly for current viewport', async ({ page }) => {
+ await expect(page.getByRole('button', { name: 'Go to home page' })).toBeVisible();
+ await expect(page.getByAltText('LFX Logo')).toBeVisible();
+
+ // Viewport-aware assertions
+ const viewport = page.viewportSize();
+ const isMobile = viewport && viewport.width < 768;
+
+ if (isMobile) {
+ // Mobile: search and brand text should be hidden
+ await expect(page.getByPlaceholder('Search projects...')).toBeHidden();
+ await expect(page.getByText('Projects Self-Service')).toBeHidden();
+ } else {
+ // Desktop: search and brand text should be visible
+ await expect(page.getByPlaceholder('Search projects...')).toBeVisible();
+ await expect(page.getByText('Projects Self-Service')).toBeVisible();
+ }
+});
+```
- // Should show meetings tab by default
- await expect(page.locator('[data-testid="meetings-content"]')).toBeVisible();
+### 4. Content-Based User Journey Testing
- // Click committees tab
- await page.click('[data-testid="committees-tab"]');
- await expect(page).toHaveURL('/project/kubernetes/committees');
- await expect(page.locator('[data-testid="committees-content"]')).toBeVisible();
+```typescript
+test('should navigate to project detail when clicking a project card', async ({ page }) => {
+ // Wait for project cards to load
+ await page.waitForLoadState('networkidle');
- // Click mailing lists tab
- await page.click('[data-testid="mailing-lists-tab"]');
- await expect(page).toHaveURL('/project/kubernetes/mailing-lists');
- await expect(page.locator('[data-testid="mailing-lists-content"]')).toBeVisible();
- });
+ const firstCard = page.locator('lfx-project-card').first();
+ await expect(firstCard).toBeVisible();
- test('should maintain navigation state on page reload', async ({ page }) => {
- await page.goto('/project/kubernetes/committees');
+ // Get project name for verification
+ const projectName = await firstCard.getByRole('heading', { level: 3 }).innerText();
- // Reload page
- await page.reload();
+ // Click the card
+ await firstCard.click();
- // Should maintain state
- await expect(page).toHaveURL('/project/kubernetes/committees');
- await expect(page.locator('[data-testid="committees-tab"]')).toHaveClass(/active/);
- await expect(page.locator('[data-testid="committees-content"]')).toBeVisible();
- });
+ // Verify navigation
+ await expect(page).toHaveURL(/\/project\/[\w-]+$/);
+ await expect(page.getByRole('heading', { level: 1 }).filter({ hasText: projectName })).toBeVisible();
});
```
-### Search and Filtering
+## ๐ฑ Multi-Browser Testing Strategy
-```typescript
-// e2e/search.spec.ts
-import { test, expect } from '@playwright/test';
-
-test.describe('Search and Filtering', () => {
- test.beforeEach(async ({ page, context }) => {
- // Mock authenticated state
- await context.addCookies([
- {
- name: 'appSession',
- value: 'mock-session-token',
- domain: 'localhost',
- path: '/',
- },
- ]);
-
- await page.goto('/');
- });
+### Browser-Specific Configurations
- test('should filter projects by search term', async ({ page }) => {
- // Wait for projects to load
- await expect(page.locator('[data-testid="project-card"]')).toHaveCount(expect.any(Number));
- const initialCount = await page.locator('[data-testid="project-card"]').count();
+#### Chromium (Desktop)
- // Enter search term
- await page.fill('[data-testid="search-input"]', 'kubernetes');
+- Full feature testing
+- Parallel execution (5 workers)
+- Complete test suite
- // Should filter results
- await expect(page.locator('[data-testid="project-card"]')).toHaveCount(expect.any(Number));
- const filteredCount = await page.locator('[data-testid="project-card"]').count();
+#### Mobile Chrome
- // Filtered count should be less than or equal to initial
- expect(filteredCount).toBeLessThanOrEqual(initialCount);
+- Single worker configuration (prevents resource contention)
+- Mobile-specific responsive validation
+- Touch interaction testing
- // All visible projects should match search term
- const projectTitles = await page.locator('[data-testid="project-card"] h3').allTextContents();
- projectTitles.forEach((title) => {
- expect(title.toLowerCase()).toContain('kubernetes');
- });
- });
+#### Firefox
- test('should clear search filter', async ({ page }) => {
- // Enter search term
- await page.fill('[data-testid="search-input"]', 'nonexistent');
+- Cross-browser compatibility validation
+- Engine-specific behavior testing
- // Should show no results
- await expect(page.locator('[data-testid="no-results"]')).toBeVisible();
+### Viewport Testing Strategy
- // Clear search
- await page.fill('[data-testid="search-input"]', '');
+```typescript
+// Mobile Viewport (< 768px)
+test('should display correctly on mobile viewport', async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+
+ // Mobile-specific expectations
+ await expect(page.getByPlaceholder('Search projects...')).toBeHidden();
+ await expect(page.getByText('Projects Self-Service')).toBeHidden();
+ await expect(page.getByAltText('LFX Logo')).toBeVisible();
+});
- // Should show all projects again
- await expect(page.locator('[data-testid="project-card"]')).toHaveCount(expect.any(Number));
- await expect(page.locator('[data-testid="no-results"]')).not.toBeVisible();
- });
+// Tablet Viewport (โฅ 768px)
+test('should display correctly on tablet viewport', async ({ page }) => {
+ await page.setViewportSize({ width: 768, height: 1024 });
+
+ // Tablet-specific expectations
+ await expect(page.getByPlaceholder('Search projects...')).toBeVisible();
+ await expect(page.getByText('Projects Self-Service')).toBeVisible();
});
```
-## ๐ฑ Responsive Testing
+## ๐ Authentication Architecture
-### Mobile and Desktop Views
+### Global Setup Strategy
```typescript
-// e2e/responsive.spec.ts
-import { test, expect } from '@playwright/test';
-
-test.describe('Responsive Design', () => {
- test('should display mobile navigation', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE
- await page.goto('/');
-
- // Mobile menu should be hidden initially
- await expect(page.locator('[data-testid="mobile-menu"]')).not.toBeVisible();
-
- // Click hamburger menu
- await page.click('[data-testid="mobile-menu-button"]');
+// e2e/helpers/global-setup.ts
+async function globalSetup(config: FullConfig) {
+ const browser = await chromium.launch();
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ try {
+ // Navigate to logout to trigger authentication flow
+ await page.goto(`${url}/logout`);
+
+ // Perform authentication
+ await AuthHelper.loginWithAuth0(page, TEST_CREDENTIALS);
+
+ // Save authentication state
+ await context.storageState({ path: 'playwright/.auth/user.json' });
+ } finally {
+ await browser.close();
+ }
+}
+```
- // Mobile menu should be visible
- await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible();
+### Auth Helper Pattern
- // Navigation items should be stacked vertically
- const menuItems = page.locator('[data-testid="mobile-menu"] [data-testid="nav-item"]');
- await expect(menuItems).toHaveCount(expect.any(Number));
- });
-
- test('should adapt project grid for different screen sizes', async ({ page }) => {
- await page.goto('/');
+```typescript
+// e2e/helpers/auth.helper.ts
+export class AuthHelper {
+ static async loginWithAuth0(page: Page, credentials: TestCredentials) {
+ // Wait for Auth0 login page
+ await page.waitForSelector('[data-testid="auth0-login-form"]');
+
+ // Fill credentials
+ await page.fill('input[name="username"]', credentials.username);
+ await page.fill('input[name="password"]', credentials.password);
+
+ // Submit and wait for redirect
+ await page.click('button[type="submit"]');
+ await page.waitForURL(/localhost:4200/);
+ }
+}
+```
- // Desktop: Should show multiple columns
- await page.setViewportSize({ width: 1200, height: 800 });
- const desktopColumns = await page.locator('[data-testid="project-grid"]').evaluate((el) => {
- return getComputedStyle(el).gridTemplateColumns.split(' ').length;
- });
+## ๐ Best Practices
- // Tablet: Should show fewer columns
- await page.setViewportSize({ width: 768, height: 1024 });
- const tabletColumns = await page.locator('[data-testid="project-grid"]').evaluate((el) => {
- return getComputedStyle(el).gridTemplateColumns.split(' ').length;
- });
+### 1. Element Selection Strategy
- // Mobile: Should show single column
- await page.setViewportSize({ width: 375, height: 667 });
- const mobileColumns = await page.locator('[data-testid="project-grid"]').evaluate((el) => {
- return getComputedStyle(el).gridTemplateColumns.split(' ').length;
- });
+#### โ
Recommended: Data-TestID
- expect(desktopColumns).toBeGreaterThan(tabletColumns);
- expect(tabletColumns).toBeGreaterThan(mobileColumns);
- expect(mobileColumns).toBe(1);
- });
-});
+```typescript
+await expect(page.locator('[data-testid="project-card"]')).toBeVisible();
```
-## โก Performance Testing
-
-### Core Web Vitals
+#### โ
Acceptable: Semantic Selectors
```typescript
-// e2e/performance.spec.ts
-import { test, expect } from '@playwright/test';
-
-test.describe('Performance', () => {
- test('should meet Core Web Vitals thresholds', async ({ page }) => {
- await page.goto('/');
-
- // Measure First Contentful Paint
- const fcpMetric = await page.evaluate(() => {
- return new Promise((resolve) => {
- new PerformanceObserver((list) => {
- const entries = list.getEntries();
- const fcp = entries.find((entry) => entry.name === 'first-contentful-paint');
- if (fcp) resolve(fcp.startTime);
- }).observe({ entryTypes: ['paint'] });
- });
- });
-
- // FCP should be under 1.8 seconds
- expect(fcpMetric).toBeLessThan(1800);
-
- // Measure Largest Contentful Paint
- const lcpMetric = await page.evaluate(() => {
- return new Promise((resolve) => {
- new PerformanceObserver((list) => {
- const entries = list.getEntries();
- const lastEntry = entries[entries.length - 1];
- resolve(lastEntry.startTime);
- }).observe({ entryTypes: ['largest-contentful-paint'] });
-
- // Fallback timeout
- setTimeout(() => resolve(0), 5000);
- });
- });
-
- // LCP should be under 2.5 seconds
- expect(lcpMetric).toBeLessThan(2500);
- });
-
- test('should load projects within acceptable time', async ({ page }) => {
- const startTime = Date.now();
+await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();
+```
- await page.goto('/');
+#### โ Avoid: CSS Classes (Tailwind)
- // Wait for projects to load
- await expect(page.locator('[data-testid="project-card"]').first()).toBeVisible();
+```typescript
+// Brittle - classes can change
+await expect(page.locator('.bg-blue-500.text-white')).toBeVisible();
+```
- const loadTime = Date.now() - startTime;
+#### โ Avoid: Generic Text Selectors
- // Should load within 3 seconds
- expect(loadTime).toBeLessThan(3000);
- });
-});
+```typescript
+// Unreliable - text can change
+await expect(page.getByText('Submit')).toBeVisible();
```
-## ๐ง Test Utilities
+### 2. Waiting Strategies
-### Page Object Model
+#### Built-in Waiting (Preferred)
```typescript
-// e2e/pages/project-page.ts
-import { Page, Locator } from '@playwright/test';
-
-export class ProjectPage {
- readonly page: Page;
- readonly projectTitle: Locator;
- readonly projectDescription: Locator;
- readonly navigationTabs: Locator;
- readonly meetingsTab: Locator;
- readonly committeesTab: Locator;
- readonly mailingListsTab: Locator;
-
- constructor(page: Page) {
- this.page = page;
- this.projectTitle = page.locator('[data-testid="project-title"]');
- this.projectDescription = page.locator('[data-testid="project-description"]');
- this.navigationTabs = page.locator('[data-testid="project-navigation"]');
- this.meetingsTab = page.locator('[data-testid="meetings-tab"]');
- this.committeesTab = page.locator('[data-testid="committees-tab"]');
- this.mailingListsTab = page.locator('[data-testid="mailing-lists-tab"]');
- }
-
- async goto(projectSlug: string) {
- await this.page.goto(`/project/${projectSlug}`);
- }
+await expect(page.locator('[data-testid="loading-spinner"]')).toBeVisible();
+await expect(page.locator('[data-testid="content"]')).toBeVisible();
+```
- async navigateToMeetings() {
- await this.meetingsTab.click();
- }
+#### Network Idle for Dynamic Content
- async navigateToCommittees() {
- await this.committeesTab.click();
- }
+```typescript
+await page.waitForLoadState('networkidle');
+const projectCards = page.locator('[data-testid="project-card"]');
+```
- async navigateToMailingLists() {
- await this.mailingListsTab.click();
- }
+### 3. Test Organization
- async getProjectTitle() {
- return await this.projectTitle.textContent();
- }
-}
+#### Descriptive Test Groups
-// Usage in tests
-test('should navigate project tabs', async ({ page }) => {
- const projectPage = new ProjectPage(page);
- await projectPage.goto('kubernetes');
+```typescript
+test.describe('Homepage - Robust Tests', () => {
+ test.describe('Page Structure and Components', () => {
+ test('should have correct page structure with main sections', async ({ page }) => {
+ // Component architecture validation
+ });
+ });
- await projectPage.navigateToCommittees();
- await expect(page).toHaveURL('/project/kubernetes/committees');
+ test.describe('Component Integration', () => {
+ test('should properly integrate Angular signals and computed values', async ({ page }) => {
+ // Framework-specific validation
+ });
+ });
});
```
-### Authentication Helpers
+### 4. Error Handling and Debugging
-```typescript
-// e2e/helpers/auth.ts
-import { Page, BrowserContext } from '@playwright/test';
-
-export async function loginUser(page: Page, context: BrowserContext) {
- // Add authentication cookie
- await context.addCookies([
- {
- name: 'appSession',
- value: 'mock-authenticated-session',
- domain: 'localhost',
- path: '/',
- httpOnly: true,
- secure: false,
- },
- ]);
-
- // Navigate to authenticated page
- await page.goto('/');
-
- // Verify authentication
- await page.waitForSelector('[data-testid="user-avatar"]');
-}
+#### Conditional Assertions
-export async function logoutUser(page: Page) {
- await page.click('[data-testid="user-avatar"]');
- await page.click('[data-testid="logout-button"]');
- await page.waitForURL(/auth0\.com/);
-}
+```typescript
+const hasProjects = await page
+ .locator('[data-testid="projects-grid"]')
+ .isVisible()
+ .catch(() => false);
+const hasSkeleton = await page
+ .locator('[data-testid="projects-skeleton"]')
+ .isVisible()
+ .catch(() => false);
+
+expect(hasProjects || hasSkeleton).toBe(true);
```
-## ๐ฏ Test Data Management
-
-### Test Data Setup
+#### Clear Error Messages
```typescript
-// e2e/fixtures/test-data.ts
-export const testProjects = [
- {
- id: 'kubernetes',
- name: 'Kubernetes',
- description: 'Container orchestration platform',
- category: 'CNCF',
- metrics: {
- meetings: 12,
- committees: 8,
- mailingLists: 15,
- },
- },
- {
- id: 'prometheus',
- name: 'Prometheus',
- description: 'Monitoring and alerting toolkit',
- category: 'CNCF',
- metrics: {
- meetings: 8,
- committees: 4,
- mailingLists: 6,
- },
- },
-];
-
-export async function seedTestData(page: Page) {
- // Mock API responses
- await page.route('/api/projects', (route) => {
- route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify(testProjects),
- });
- });
-}
+const cardCount = await projectCards.count();
+expect(cardCount).toBeGreaterThan(0, 'Should have at least one project card');
```
-## ๐ CI/CD Integration
+## ๐ Maintenance and Monitoring
-### GitHub Actions Workflow
+### Test Health Metrics
-```yaml
-# .github/workflows/e2e-tests.yml
-name: E2E Tests
+#### Current Status: โ
85/85 tests passing
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
+1. **Reliability**: Zero flaky tests
+2. **Performance**: Average test suite runs in ~54 seconds (Chromium)
+3. **Coverage**: All major user journeys covered
+4. **Maintainability**: Data-testid architecture prevents UI change breakage
-jobs:
- e2e:
- runs-on: ubuntu-latest
+### Test Maintenance Schedule
- steps:
- - uses: actions/checkout@v3
+**Weekly**: Run full test suite across all browsers
+**Per PR**: Automated test execution in CI/CD
+**Monthly**: Review and update test documentation
+**Quarterly**: Evaluate new testing patterns and tools
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '18'
+### Debugging Guidelines
- - name: Install dependencies
- run: yarn install
+1. **Screenshot Analysis**: Use Playwright's built-in screenshot capture
+2. **Trace Files**: Leverage trace viewer for step-by-step debugging
+3. **Network Analysis**: Monitor API calls and responses
+4. **Console Logs**: Check for JavaScript errors
+5. **Element Inspection**: Validate data-testid attributes in dev tools
- - name: Build application
- run: yarn build
+## ๐ Implementation Checklist
- - name: Install Playwright
- run: yarn playwright install --with-deps
+### โ
Completed
- - name: Run E2E tests
- run: yarn playwright test
+- [x] Dual testing architecture (content + structural)
+- [x] Data-testid implementation across components
+- [x] Multi-browser configuration (Chromium, Mobile Chrome)
+- [x] Responsive design testing
+- [x] Authentication flow with global setup
+- [x] Angular signals integration testing
+- [x] Component architecture validation
- - name: Upload test results
- uses: actions/upload-artifact@v3
- if: failure()
- with:
- name: playwright-report
- path: playwright-report/
-```
+### ๐ฒ Future Enhancements
-## ๐ Best Practices
+- [ ] Visual regression testing with screenshot comparison
+- [ ] Accessibility testing with axe-core integration
+- [ ] Performance testing with Core Web Vitals
+- [ ] Cross-platform testing (Windows, macOS, Linux)
+- [ ] Test reporting dashboard with historical data
-### E2E Testing Guidelines
+## ๐ฏ Testing Guidelines for New Features
-1. **Test User Journeys**: Focus on complete user workflows
-2. **Use Data Test IDs**: Reliable element selection
-3. **Avoid Flaky Tests**: Proper waiting strategies
-4. **Test Critical Paths**: Cover most important user flows
-5. **Keep Tests Independent**: Each test should be isolated
+When adding new features, follow this testing approach:
-### Performance Optimization
+### 1. Add Data-TestID Attributes
-1. **Parallel Execution**: Run tests in parallel when possible
-2. **Smart Waiting**: Use built-in wait strategies
-3. **Resource Management**: Clean up browser resources
-4. **Test Data**: Use efficient test data setup
-5. **CI Optimization**: Optimize for CI/CD environments
+```html
+
+
+```
-### Debugging and Maintenance
+### 2. Create Both Test Types
-1. **Visual Debugging**: Use screenshots and videos
-2. **Trace Analysis**: Leverage Playwright traces
-3. **Error Reporting**: Clear error messages
-4. **Test Stability**: Regular test maintenance
-5. **Documentation**: Document test scenarios
+#### Content-Based Test (User Experience)
-## ๐ Implementation Status
+```typescript
+test('should allow user to complete new feature workflow', async ({ page }) => {
+ // Test user-visible behavior and interactions
+});
+```
-### โ
Ready for Implementation
+#### Structural Test (Technical Implementation)
-- Playwright configuration
-- Authentication flow testing
-- Navigation testing
-- Responsive design testing
-- Performance testing patterns
+```typescript
+test('should use correct component architecture for new feature', async ({ page }) => {
+ // Test component structure and framework integration
+});
+```
-### ๐ฒ Future Enhancements
+### 3. Include Responsive Testing
-- Cross-browser testing matrix
-- Visual regression testing
-- Accessibility testing
-- Load testing integration
-- Test reporting dashboard
+```typescript
+test('should display new feature correctly across viewports', async ({ page }) => {
+ // Test mobile, tablet, and desktop layouts
+});
+```
-This E2E testing strategy ensures comprehensive coverage of user workflows and provides confidence in the application's behavior across different browsers and devices.
+This comprehensive E2E testing architecture ensures reliable, maintainable tests that provide confidence in both user experience and technical implementation while surviving UI changes and framework updates.
diff --git a/docs/architecture/testing/integration-testing.md b/docs/architecture/testing/integration-testing.md
deleted file mode 100644
index 85806994..00000000
--- a/docs/architecture/testing/integration-testing.md
+++ /dev/null
@@ -1,526 +0,0 @@
-# Integration Testing
-
-## ๐ Integration Testing Strategy
-
-Integration tests verify that different parts of the application work correctly together, focusing on API endpoints, service interactions, and data flow.
-
-## ๐ Backend Integration Testing
-
-### Express Server Testing
-
-```typescript
-// Example: server.integration.spec.ts
-import request from 'supertest';
-import { app } from '../src/server/server';
-
-describe('Server Integration', () => {
- let server: any;
-
- beforeAll(() => {
- server = app();
- });
-
- describe('Health Endpoint', () => {
- it('should return OK', async () => {
- const response = await request(server).get('/health').expect(200);
-
- expect(response.text).toBe('OK');
- });
-
- it('should not log health checks', async () => {
- // Verify health endpoint is excluded from logging
- const logSpy = jest.spyOn(console, 'log');
-
- await request(server).get('/health');
-
- expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining('/health'));
- });
- });
-
- describe('Authentication', () => {
- it('should redirect unauthenticated requests to Auth0', async () => {
- const response = await request(server).get('/dashboard').expect(302);
-
- expect(response.headers.location).toContain('auth0.com');
- });
-
- it('should serve protected content for authenticated users', async () => {
- // Mock authentication middleware
- const mockAuth = jest.fn((req, res, next) => {
- req.oidc = {
- isAuthenticated: () => true,
- user: {
- sub: 'test-user-123',
- name: 'Test User',
- email: 'test@example.com',
- },
- };
- next();
- });
-
- // This would require mocking the auth middleware
- // Implementation depends on testing setup
- });
- });
-});
-```
-
-### API Route Testing
-
-```typescript
-// Example: api.integration.spec.ts
-import request from 'supertest';
-import { app } from '../src/server/server';
-
-describe('API Integration', () => {
- let server: any;
-
- beforeAll(() => {
- server = app();
- });
-
- describe('Project API', () => {
- it('should return projects list', async () => {
- // This assumes API endpoints are implemented
- const response = await request(server).get('/api/projects').set('Authorization', 'Bearer valid-token').expect(200);
-
- expect(response.body).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- id: expect.any(String),
- name: expect.any(String),
- description: expect.any(String),
- }),
- ])
- );
- });
-
- it('should handle authentication errors', async () => {
- await request(server).get('/api/projects').expect(401);
- });
-
- it('should validate request parameters', async () => {
- await request(server).post('/api/projects').send({ invalidData: true }).set('Authorization', 'Bearer valid-token').expect(400);
- });
- });
-});
-```
-
-## ๐ฏ Frontend Integration Testing
-
-### Component-Service Integration
-
-```typescript
-// Example: project-list.integration.spec.ts
-import { TestBed } from '@angular/core/testing';
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { ComponentFixture } from '@angular/core/testing';
-import { ProjectListComponent } from './project-list.component';
-import { ProjectService } from '../services/project.service';
-
-describe('ProjectListComponent Integration', () => {
- let component: ProjectListComponent;
- let fixture: ComponentFixture
;
- let httpMock: HttpTestingController;
- let projectService: ProjectService;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [ProjectListComponent, HttpClientTestingModule],
- providers: [ProjectService],
- }).compileComponents();
-
- fixture = TestBed.createComponent(ProjectListComponent);
- component = fixture.componentInstance;
- httpMock = TestBed.inject(HttpTestingController);
- projectService = TestBed.inject(ProjectService);
- });
-
- afterEach(() => {
- httpMock.verify();
- });
-
- it('should load and display projects', async () => {
- const mockProjects = [
- { id: '1', name: 'Kubernetes', description: 'Container orchestration' },
- { id: '2', name: 'Prometheus', description: 'Monitoring system' },
- ];
-
- // Trigger component initialization
- fixture.detectChanges();
-
- // Expect HTTP request
- const req = httpMock.expectOne('/api/projects');
- expect(req.request.method).toBe('GET');
- req.flush(mockProjects);
-
- // Wait for async operations
- fixture.detectChanges();
- await fixture.whenStable();
-
- // Verify UI updates
- const projectCards = fixture.nativeElement.querySelectorAll('lfx-project-card');
- expect(projectCards.length).toBe(2);
- expect(projectCards[0].textContent).toContain('Kubernetes');
- expect(projectCards[1].textContent).toContain('Prometheus');
- });
-
- it('should handle loading states', () => {
- fixture.detectChanges();
-
- // Should show loading state
- expect(component.loading()).toBe(true);
- const loadingElement = fixture.nativeElement.querySelector('.loading');
- expect(loadingElement).toBeTruthy();
-
- // Complete the request
- const req = httpMock.expectOne('/api/projects');
- req.flush([]);
-
- fixture.detectChanges();
-
- // Should hide loading state
- expect(component.loading()).toBe(false);
- const loadingElementAfter = fixture.nativeElement.querySelector('.loading');
- expect(loadingElementAfter).toBeFalsy();
- });
-
- it('should handle errors gracefully', () => {
- fixture.detectChanges();
-
- const req = httpMock.expectOne('/api/projects');
- req.error(new ErrorEvent('Network error'), { status: 500 });
-
- fixture.detectChanges();
-
- expect(component.error()).toBeTruthy();
- const errorElement = fixture.nativeElement.querySelector('.error');
- expect(errorElement).toBeTruthy();
- expect(errorElement.textContent).toContain('error');
- });
-});
-```
-
-### Router Integration Testing
-
-```typescript
-// Example: routing.integration.spec.ts
-import { TestBed } from '@angular/core/testing';
-import { Router } from '@angular/router';
-import { Location } from '@angular/common';
-import { Component } from '@angular/core';
-import { routes } from '../app.routes';
-
-@Component({ template: '' })
-class TestComponent {}
-
-describe('Routing Integration', () => {
- let router: Router;
- let location: Location;
- let fixture: any;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [
- RouterTestingModule.withRoutes([
- { path: '', component: TestComponent },
- { path: 'project/:id', component: TestComponent },
- { path: 'project/:id/meetings', component: TestComponent },
- ]),
- ],
- declarations: [TestComponent],
- }).compileComponents();
-
- router = TestBed.inject(Router);
- location = TestBed.inject(Location);
- fixture = TestBed.createComponent(TestComponent);
- });
-
- it('should navigate to home', async () => {
- await router.navigate(['']);
- expect(location.path()).toBe('');
- });
-
- it('should navigate to project page', async () => {
- await router.navigate(['/project', 'kubernetes']);
- expect(location.path()).toBe('/project/kubernetes');
- });
-
- it('should navigate to project meetings', async () => {
- await router.navigate(['/project', 'kubernetes', 'meetings']);
- expect(location.path()).toBe('/project/kubernetes/meetings');
- });
-});
-```
-
-## ๐ SSR Integration Testing
-
-### Server-Side Rendering Tests
-
-```typescript
-// Example: ssr.integration.spec.ts
-import request from 'supertest';
-import { app } from '../src/server/server';
-
-describe('SSR Integration', () => {
- let server: any;
-
- beforeAll(() => {
- server = app();
- });
-
- it('should serve server-rendered HTML', async () => {
- const response = await request(server).get('/').expect(200).expect('Content-Type', /html/);
-
- // Verify SSR content
- expect(response.text).toContain('');
- expect(response.text).toContain(' {
- // Mock authenticated request
- const response = await request(server).get('/dashboard').set('Cookie', 'mock-auth-cookie=value').expect(200);
-
- // Verify auth context is injected
- expect(response.text).toContain('authenticated: true');
- });
-
- it('should handle SSR errors gracefully', async () => {
- // Request non-existent route
- const response = await request(server).get('/non-existent-route').expect(404);
-
- expect(response.text).toContain('Not Found');
- });
-});
-```
-
-## ๐ Database Integration Testing
-
-### Mock Database Testing
-
-```typescript
-// Example: database.integration.spec.ts
-import { TestContainer, StartedTestContainer } from 'testcontainers';
-
-describe('Database Integration', () => {
- let container: StartedTestContainer;
- let databaseUrl: string;
-
- beforeAll(async () => {
- // Start test database container
- container = await new TestContainer('postgres:15').withEnvironment({ POSTGRES_PASSWORD: 'test' }).withExposedPorts(5432).start();
-
- const port = container.getMappedPort(5432);
- databaseUrl = `postgresql://postgres:test@localhost:${port}/postgres`;
- });
-
- afterAll(async () => {
- await container.stop();
- });
-
- it('should connect to database', async () => {
- // This would test actual database operations
- // Currently not implemented as we use direct API calls
- expect(databaseUrl).toBeDefined();
- });
-});
-```
-
-## ๐ฏ Authentication Integration Testing
-
-### Auth0 Integration
-
-```typescript
-// Example: auth.integration.spec.ts
-import request from 'supertest';
-import { app } from '../src/server/server';
-
-describe('Authentication Integration', () => {
- let server: any;
-
- beforeAll(() => {
- server = app();
- });
-
- it('should redirect to Auth0 for login', async () => {
- const response = await request(server).get('/login').expect(302);
-
- expect(response.headers.location).toMatch(/auth0\.com/);
- });
-
- it('should handle Auth0 callback', async () => {
- // Mock Auth0 callback with authorization code
- const response = await request(server)
- .get('/callback')
- .query({
- code: 'mock-auth-code',
- state: 'mock-state',
- })
- .expect(302);
-
- // Should redirect to application
- expect(response.headers.location).not.toMatch(/auth0\.com/);
- });
-
- it('should maintain session after authentication', async () => {
- // This would require session management testing
- // Implementation depends on session storage
- });
-});
-```
-
-## ๐ง Testing Utilities
-
-### Integration Test Helpers
-
-```typescript
-// testing/integration-helpers.ts
-import { TestBed } from '@angular/core/testing';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-import { NoopAnimationsModule } from '@angular/platform-browser/animations';
-
-export function setupIntegrationTest(
- options: {
- imports?: any[];
- providers?: any[];
- declarations?: any[];
- } = {}
-) {
- return TestBed.configureTestingModule({
- imports: [HttpClientTestingModule, RouterTestingModule, NoopAnimationsModule, ...(options.imports || [])],
- providers: options.providers || [],
- declarations: options.declarations || [],
- });
-}
-
-export function createMockUser() {
- return {
- sub: 'test-user-123',
- name: 'Test User',
- email: 'test@example.com',
- given_name: 'Test',
- family_name: 'User',
- nickname: 'testuser',
- picture: 'https://example.com/avatar.jpg',
- updated_at: '2023-01-01T00:00:00.000Z',
- email_verified: true,
- sid: 'test-session-123',
- 'https://sso.linuxfoundation.org/claims/username': 'testuser',
- };
-}
-
-export function mockAuthContext(authenticated: boolean = true) {
- return {
- authenticated,
- user: authenticated ? createMockUser() : null,
- };
-}
-```
-
-### API Mock Utilities
-
-```typescript
-// testing/api-mocks.ts
-import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-import { of } from 'rxjs';
-
-@Injectable()
-export class MockApiInterceptor implements HttpInterceptor {
- private mocks = new Map();
-
- intercept(req: HttpRequest, next: HttpHandler) {
- const mockResponse = this.mocks.get(req.url);
-
- if (mockResponse) {
- return of(mockResponse);
- }
-
- return next.handle(req);
- }
-
- mockEndpoint(url: string, response: any) {
- this.mocks.set(url, response);
- }
-
- clearMocks() {
- this.mocks.clear();
- }
-}
-
-// Usage in tests
-beforeEach(() => {
- const mockInterceptor = TestBed.inject(MockApiInterceptor);
- mockInterceptor.mockEndpoint('/api/projects', [{ id: '1', name: 'Test Project' }]);
-});
-```
-
-## ๐ฏ Integration Testing Best Practices
-
-### Testing Guidelines
-
-1. **Test Real Interactions**: Use actual HTTP calls and routing
-2. **Mock External Services**: Mock Auth0, databases, external APIs
-3. **Test Error Scenarios**: Network failures, authentication errors
-4. **Verify Data Flow**: From API through services to components
-5. **Test State Management**: Signal updates across component boundaries
-
-### Performance Considerations
-
-1. **Parallel Test Execution**: Run tests concurrently when possible
-2. **Test Data Isolation**: Ensure tests don't interfere with each other
-3. **Resource Cleanup**: Properly clean up test resources
-4. **Container Management**: Efficiently manage test containers
-
-### CI/CD Integration
-
-```yaml
-# Example: .github/workflows/integration-tests.yml
-name: Integration Tests
-
-on: [push, pull_request]
-
-jobs:
- integration:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Setup Node.js
- uses: actions/setup-node@v3
- with:
- node-version: '18'
-
- - name: Install dependencies
- run: yarn install
-
- - name: Build shared packages
- run: yarn build
-
- - name: Run integration tests
- run: yarn test:integration
- env:
- CI: true
-```
-
-## ๐ Implementation Status
-
-### โ
Ready for Implementation
-
-- Express server testing patterns
-- Component-service integration
-- SSR testing strategies
-- Authentication flow testing
-
-### ๐ฒ Future Enhancements
-
-- Database integration tests
-- External API integration
-- Performance integration tests
-- Cross-browser integration testing
-- Load testing integration
-
-This integration testing strategy ensures that all parts of the application work correctly together and provides confidence in the overall system behavior.
diff --git a/docs/architecture/testing/testing-best-practices.md b/docs/architecture/testing/testing-best-practices.md
new file mode 100644
index 00000000..3e25d71a
--- /dev/null
+++ b/docs/architecture/testing/testing-best-practices.md
@@ -0,0 +1,555 @@
+# Testing Best Practices Guide
+
+## ๐ฏ Overview
+
+This guide outlines testing best practices for the LFX Projects Self-Service application, covering our dual testing architecture, data-testid conventions, and implementation guidelines.
+
+## ๐ Dual Testing Architecture Principles
+
+### When to Use Content-Based Tests
+
+**Purpose**: Validate user experience and acceptance criteria
+
+**Use Cases**:
+
+- User journey testing (login โ search โ navigate โ interact)
+- Content validation (text, labels, messages)
+- Accessibility testing (screen reader compatibility)
+- Cross-browser functionality testing
+
+**Example**:
+
+```typescript
+test('should complete project search workflow', async ({ page }) => {
+ // User sees and interacts with visible elements
+ await expect(page.getByRole('textbox', { name: 'Search projects' })).toBeVisible();
+ await page.fill('input[placeholder*="Search"]', 'kubernetes');
+ await expect(page.getByText('Kubernetes')).toBeVisible();
+});
+```
+
+### When to Use Structural Tests
+
+**Purpose**: Validate technical implementation and component architecture
+
+**Use Cases**:
+
+- Component integration testing (Angular signals, computed values)
+- Framework-specific validation (custom components, directives)
+- UI library independence (ensuring wrapper components work)
+- Architecture compliance (consistent component usage)
+
+**Example**:
+
+```typescript
+test('should use lfx-card components consistently', async ({ page }) => {
+ // Technical validation of component architecture
+ const cards = page.locator('[data-testid="project-card"]');
+ const firstCard = cards.first();
+
+ const tagName = await firstCard.evaluate((el) => el.tagName.toLowerCase());
+ expect(tagName).toBe('lfx-project-card');
+});
+```
+
+## ๐ง Data-TestID Implementation Guide
+
+### Naming Conventions
+
+**Hierarchical Structure**:
+
+```text
+[section]-[component]-[element]
+hero-search-input
+metrics-cards-container
+project-health-card
+```
+
+**Category Guidelines**:
+
+1. **Sections**: `hero-section`, `projects-section`, `footer-section`
+2. **Containers**: `metrics-cards-container`, `projects-grid`, `navigation-menu`
+3. **Components**: `project-card`, `search-input`, `user-avatar`
+4. **Elements**: `project-title`, `metric-value`, `loading-spinner`
+5. **Actions**: `submit-button`, `cancel-link`, `edit-icon`
+
+### Implementation Examples
+
+**Basic Component**:
+
+```html
+
+
+
+ 85%
+ Activity Score
+
+
+
+```
+
+**Dynamic Attributes**:
+
+```html
+
+```
+
+**Lists and Collections**:
+
+```html
+
+ @for (metric of metrics; track metric.id) {
+
+ {{ metric.label }}
+ {{ metric.value }}
+
+ }
+
+```
+
+## ๐ฑ Responsive Testing Strategy
+
+### Viewport Breakpoints
+
+**Mobile**: `< 768px` (iPhone, Android)
+**Tablet**: `768px - 1024px` (iPad, tablets)
+**Desktop**: `> 1024px` (laptops, monitors)
+
+### Implementation Pattern
+
+```typescript
+test('should adapt to viewport size', async ({ page }) => {
+ const viewport = page.viewportSize();
+ const isMobile = viewport && viewport.width < 768;
+ const isTablet = viewport && viewport.width >= 768 && viewport.width < 1024;
+ const isDesktop = viewport && viewport.width >= 1024;
+
+ if (isMobile) {
+ // Mobile-specific expectations
+ await expect(page.getByPlaceholder('Search projects...')).toBeHidden();
+ await expect(page.locator('[data-testid="mobile-menu-button"]')).toBeVisible();
+ } else if (isTablet) {
+ // Tablet-specific expectations
+ await expect(page.getByPlaceholder('Search projects...')).toBeVisible();
+ await expect(page.locator('[data-testid="desktop-navigation"]')).toBeVisible();
+ } else {
+ // Desktop-specific expectations
+ await expect(page.locator('[data-testid="full-navigation"]')).toBeVisible();
+ await expect(page.locator('[data-testid="sidebar"]')).toBeVisible();
+ }
+});
+```
+
+### Responsive Test Organization
+
+```typescript
+test.describe('Responsive Design', () => {
+ test('should display correctly on mobile', async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ // Mobile tests
+ });
+
+ test('should display correctly on tablet', async ({ page }) => {
+ await page.setViewportSize({ width: 768, height: 1024 });
+ // Tablet tests
+ });
+
+ test('should display correctly on desktop', async ({ page }) => {
+ await page.setViewportSize({ width: 1200, height: 800 });
+ // Desktop tests
+ });
+});
+```
+
+## ๐งช Angular-Specific Testing Patterns
+
+### Signals Integration Testing
+
+```typescript
+test('should properly integrate Angular signals', async ({ page }) => {
+ // Wait for Angular to initialize and signals to resolve
+ await page.waitForLoadState('networkidle');
+
+ // Test computed signal results
+ const metricsCards = page.locator('[data-testid="metrics-cards-container"]');
+ await expect(metricsCards.locator('[data-testid="total-members-card"]')).toBeVisible();
+
+ // Test signal reactivity (if search triggers signal updates)
+ await page.fill('[data-testid="search-input"]', 'test');
+ await page.waitForTimeout(300); // Debounce time
+
+ // Verify signal-driven UI updates
+ const filteredResults = page.locator('[data-testid="project-card"]');
+ expect(await filteredResults.count()).toBeGreaterThanOrEqual(0);
+});
+```
+
+### Component Architecture Validation
+
+```typescript
+test('should use custom LFX components consistently', async ({ page }) => {
+ const componentsToValidate = [
+ { selector: '[data-testid="hero-search-input"]', expected: 'lfx-input-text' },
+ { selector: '[data-testid="project-card"]', expected: 'lfx-project-card' },
+ { selector: '[data-testid="navigation-menu"]', expected: 'lfx-menu' },
+ ];
+
+ for (const { selector, expected } of componentsToValidate) {
+ const element = page.locator(selector);
+ await expect(element).toBeVisible();
+
+ const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
+ expect(tagName).toBe(expected);
+ }
+});
+```
+
+### Form Integration Testing
+
+```typescript
+test('should integrate Angular reactive forms', async ({ page }) => {
+ const searchForm = page.locator('[data-testid="search-form"]');
+ const searchInput = page.locator('[data-testid="search-input"]');
+
+ // Test form validation
+ await searchInput.fill('ab'); // Too short
+ await expect(page.locator('[data-testid="search-error"]')).toBeVisible();
+
+ // Test valid input
+ await searchInput.fill('kubernetes');
+ await expect(page.locator('[data-testid="search-error"]')).not.toBeVisible();
+
+ // Test form submission
+ await page.keyboard.press('Enter');
+ await expect(page.locator('[data-testid="search-results"]')).toBeVisible();
+});
+```
+
+## ๐ Element Selection Best Practices
+
+### Recommended Approaches (In Priority Order)
+
+#### 1. Data-TestID (Highest Priority)
+
+```typescript
+// โ
Most reliable - survives UI changes
+await expect(page.locator('[data-testid="project-card"]')).toBeVisible();
+```
+
+#### 2. Semantic Role-Based
+
+```typescript
+// โ
Good for accessibility and semantics
+await expect(page.getByRole('button', { name: 'Save Project' })).toBeVisible();
+await expect(page.getByRole('heading', { level: 1 })).toContainText('Dashboard');
+```
+
+#### 3. Label-Based
+
+```typescript
+// โ
Good for form elements
+await expect(page.getByLabel('Project Name')).toBeVisible();
+await expect(page.getByPlaceholder('Enter project name')).toBeFocused();
+```
+
+#### 4. Text Content (Use Sparingly)
+
+```typescript
+// โ ๏ธ Use only for unique, stable text
+await expect(page.getByText('No projects found')).toBeVisible();
+```
+
+### Approaches to Avoid
+
+#### CSS Classes (Especially Tailwind)
+
+```typescript
+// โ Brittle - classes change frequently
+await expect(page.locator('.bg-blue-500.text-white')).toBeVisible();
+```
+
+#### Complex CSS Selectors
+
+```typescript
+// โ Fragile - DOM structure can change
+await expect(page.locator('div > div:nth-child(2) > span')).toBeVisible();
+```
+
+#### Generic Selectors
+
+```typescript
+// โ Non-specific - multiple matches possible
+await expect(page.locator('button')).toBeVisible();
+```
+
+## โฑ Waiting Strategies
+
+### Built-in Waiting (Preferred)
+
+```typescript
+// โ
Auto-waits until element is visible
+await expect(page.locator('[data-testid="content"]')).toBeVisible();
+
+// โ
Auto-waits until element is clickable
+await page.click('[data-testid="submit-button"]');
+
+// โ
Auto-waits until URL matches
+await expect(page).toHaveURL(/\/project\/\w+/);
+```
+
+### Network-Based Waiting
+
+```typescript
+// โ
Wait for network requests to complete
+await page.waitForLoadState('networkidle');
+
+// โ
Wait for specific API responses
+await page.waitForResponse((response) => response.url().includes('/api/projects') && response.status() === 200);
+```
+
+### Conditional Waiting
+
+```typescript
+// โ
Handle optional elements gracefully
+const loadingSpinner = page.locator('[data-testid="loading-spinner"]');
+const isLoading = await loadingSpinner.isVisible().catch(() => false);
+
+if (isLoading) {
+ await expect(loadingSpinner).not.toBeVisible();
+}
+
+await expect(page.locator('[data-testid="content"]')).toBeVisible();
+```
+
+### Avoid Fixed Timeouts
+
+```typescript
+// โ Brittle and unreliable
+await page.waitForTimeout(5000);
+
+// โ
Better - wait for specific condition
+await expect(page.locator('[data-testid="data-loaded"]')).toBeVisible();
+```
+
+## ๐ Test Organization Patterns
+
+### Descriptive Test Structure
+
+```typescript
+test.describe('Feature Name - Test Type', () => {
+ test.describe('Functional Area', () => {
+ test('should perform specific action with expected outcome', async ({ page }) => {
+ // Test implementation
+ });
+ });
+});
+```
+
+**Example**:
+
+```typescript
+test.describe('Project Dashboard - Robust Tests', () => {
+ test.describe('Metrics Cards', () => {
+ test('should display all four metrics cards with proper structure', async ({ page }) => {
+ // Structural validation
+ });
+
+ test('should display metrics with proper labels and values', async ({ page }) => {
+ // Content validation
+ });
+ });
+
+ test.describe('Component Integration', () => {
+ test('should properly integrate Angular signals and computed values', async ({ page }) => {
+ // Framework validation
+ });
+ });
+});
+```
+
+### BeforeEach Patterns
+
+```typescript
+test.describe('Feature Tests', () => {
+ test.beforeEach(async ({ page }) => {
+ // Navigate to page
+ await page.goto('/feature');
+
+ // Wait for initial load
+ await expect(page.locator('[data-testid="main-content"]')).toBeVisible();
+
+ // Setup test data if needed
+ await page.route('/api/test-data', (route) => {
+ route.fulfill({ json: mockData });
+ });
+ });
+
+ // Tests...
+});
+```
+
+## ๐ Error Handling and Debugging
+
+### Conditional Assertions
+
+```typescript
+test('should handle optional content gracefully', async ({ page }) => {
+ // Check for either content or empty state
+ const hasContent = await page
+ .locator('[data-testid="content"]')
+ .isVisible()
+ .catch(() => false);
+ const hasEmptyState = await page
+ .locator('[data-testid="empty-state"]')
+ .isVisible()
+ .catch(() => false);
+
+ expect(hasContent || hasEmptyState).toBe(true);
+
+ if (hasContent) {
+ // Test content-specific functionality
+ await expect(page.locator('[data-testid="content-item"]')).toHaveCount(expect.any(Number));
+ } else {
+ // Test empty state functionality
+ await expect(page.locator('[data-testid="empty-message"]')).toBeVisible();
+ }
+});
+```
+
+### Error Context and Messages
+
+```typescript
+test('should validate with clear error messages', async ({ page }) => {
+ const projectCards = page.locator('[data-testid="project-card"]');
+ const cardCount = await projectCards.count();
+
+ // Provide context in assertions
+ expect(cardCount).toBeGreaterThan(0, 'Should have at least one project card after loading');
+
+ // Test each card structure
+ for (let i = 0; i < cardCount; i++) {
+ const card = projectCards.nth(i);
+ await expect(card.locator('[data-testid="project-title"]')).toBeVisible({
+ message: `Project card ${i} should have a visible title`,
+ });
+ }
+});
+```
+
+### Debug Information Collection
+
+```typescript
+test('should collect debug info on failure', async ({ page }) => {
+ try {
+ await expect(page.locator('[data-testid="critical-element"]')).toBeVisible();
+ } catch (error) {
+ // Collect debug information
+ const url = page.url();
+ const title = await page.title();
+ const screenshot = await page.screenshot();
+
+ console.log(`Test failed on page: ${url}, title: ${title}`);
+ throw error;
+ }
+});
+```
+
+## ๐ Performance Testing Integration
+
+### Core Web Vitals
+
+```typescript
+test('should meet performance thresholds', async ({ page }) => {
+ const startTime = Date.now();
+
+ await page.goto('/');
+
+ // Wait for main content
+ await expect(page.locator('[data-testid="main-content"]')).toBeVisible();
+
+ const loadTime = Date.now() - startTime;
+ expect(loadTime).toBeLessThan(3000, 'Page should load within 3 seconds');
+
+ // Test Largest Contentful Paint
+ const lcp = await page.evaluate(() => {
+ return new Promise((resolve) => {
+ new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ const lastEntry = entries[entries.length - 1];
+ resolve(lastEntry.startTime);
+ }).observe({ entryTypes: ['largest-contentful-paint'] });
+
+ setTimeout(() => resolve(0), 5000);
+ });
+ });
+
+ expect(lcp).toBeLessThan(2500, 'LCP should be under 2.5 seconds');
+});
+```
+
+## ๐ Test Maintenance Guidelines
+
+### Regular Review Checklist
+
+**Weekly**:
+
+- [ ] Run full test suite on all browsers
+- [ ] Check for flaky tests and investigate
+- [ ] Review test execution times
+
+**Monthly**:
+
+- [ ] Update test documentation
+- [ ] Review data-testid naming consistency
+- [ ] Evaluate new testing patterns
+- [ ] Clean up obsolete tests
+
+**Per Feature**:
+
+- [ ] Add both content-based and structural tests
+- [ ] Include responsive design validation
+- [ ] Test Angular integration points
+- [ ] Update test documentation
+
+### Code Review Checklist
+
+**For New Tests**:
+
+- [ ] Uses data-testid attributes appropriately
+- [ ] Includes both test types where relevant
+- [ ] Has proper waiting strategies
+- [ ] Follows naming conventions
+- [ ] Includes error handling
+
+**For Test Updates**:
+
+- [ ] Maintains backward compatibility
+- [ ] Updates related documentation
+- [ ] Preserves test coverage
+- [ ] Improves reliability
+
+## ๐ฏ Implementation Guidelines
+
+### Adding Tests for New Features
+
+1. **Plan Test Architecture**
+ - Identify user journeys (content-based tests)
+ - Identify technical validations (structural tests)
+ - Consider responsive requirements
+
+2. **Add Data-TestID Attributes**
+ - Follow naming conventions
+ - Add hierarchical structure
+ - Include dynamic attributes where needed
+
+3. **Implement Tests**
+ - Start with structural tests (faster to run)
+ - Add content-based tests for user journeys
+ - Include responsive validation
+
+4. **Validate and Document**
+ - Run tests across all browsers
+ - Update relevant documentation
+ - Add to maintenance schedule
+
+This comprehensive testing approach ensures reliable, maintainable tests that provide confidence in both user experience and technical implementation while supporting long-term project sustainability.
diff --git a/docs/architecture/testing/unit-testing.md b/docs/architecture/testing/unit-testing.md
deleted file mode 100644
index cf07c9ae..00000000
--- a/docs/architecture/testing/unit-testing.md
+++ /dev/null
@@ -1,430 +0,0 @@
-# Unit Testing
-
-## ๐งช Current Testing Status
-
-Currently, the application has testing disabled in the Angular CLI configuration to focus on initial development. Test scaffolding is skipped for new components and services.
-
-### Angular CLI Configuration
-
-```json
-// apps/lfx-pcc/angular.json
-"schematics": {
- "@schematics/angular:component": {
- "skipTests": true
- },
- "@schematics/angular:service": {
- "skipTests": true
- },
- "@schematics/angular:guard": {
- "skipTests": true
- }
-}
-```
-
-## ๐ Future Testing Strategy
-
-When testing is implemented, the application will use Angular's recommended testing tools and patterns.
-
-## ๐ง Planned Testing Tools
-
-### Frontend Testing Stack
-
-- **Jest**: Primary testing framework for Angular applications
-- **Angular Testing Utilities**: TestBed, ComponentFixture, async testing
-- **Testing Library**: User-centric testing approaches
-- **MSW (Mock Service Worker)**: API mocking for integration tests
-
-### Test Types
-
-- **Component Tests**: UI logic and user interactions
-- **Service Tests**: Business logic and data handling
-- **Pipe Tests**: Data transformation logic
-- **Guard Tests**: Route protection logic
-
-## ๐ Component Testing Patterns
-
-### Signal-Based Component Testing
-
-```typescript
-// Example: project-card.component.spec.ts
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { signal } from '@angular/core';
-import { ProjectCardComponent } from './project-card.component';
-
-describe('ProjectCardComponent', () => {
- let component: ProjectCardComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [ProjectCardComponent],
- }).compileComponents();
-
- fixture = TestBed.createComponent(ProjectCardComponent);
- component = fixture.componentInstance;
- });
-
- it('should display project title', () => {
- // Set input signals
- fixture.componentRef.setInput('title', 'Test Project');
- fixture.componentRef.setInput('description', 'Test Description');
-
- fixture.detectChanges();
-
- const titleElement = fixture.nativeElement.querySelector('h3');
- expect(titleElement.textContent).toBe('Test Project');
- });
-
- it('should emit click events', () => {
- spyOn(component.onClick, 'emit');
-
- const cardElement = fixture.nativeElement.querySelector('.project-card');
- cardElement.click();
-
- expect(component.onClick.emit).toHaveBeenCalled();
- });
-});
-```
-
-### Testing PrimeNG Wrapper Components
-
-```typescript
-// Example: avatar.component.spec.ts
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { AvatarComponent } from './avatar.component';
-import { AvatarModule } from 'primeng/avatar';
-
-describe('AvatarComponent', () => {
- let component: AvatarComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [AvatarComponent, AvatarModule],
- }).compileComponents();
-
- fixture = TestBed.createComponent(AvatarComponent);
- component = fixture.componentInstance;
- });
-
- it('should display image when provided', () => {
- fixture.componentRef.setInput('image', 'test-image.jpg');
- fixture.detectChanges();
-
- expect(component.displayImage()).toBe('test-image.jpg');
- expect(component.displayIcon()).toBe('');
- expect(component.displayLabel()).toBe('');
- });
-
- it('should fallback to icon when image fails', () => {
- fixture.componentRef.setInput('image', 'broken-image.jpg');
- fixture.componentRef.setInput('icon', 'fa-user');
-
- // Simulate image error
- component.handleImageError();
- fixture.detectChanges();
-
- expect(component.displayImage()).toBe('');
- expect(component.displayIcon()).toBe('fa-user');
- });
-
- it('should fallback to label when no image or icon', () => {
- fixture.componentRef.setInput('label', 'John Doe');
- fixture.detectChanges();
-
- expect(component.displayImage()).toBe('');
- expect(component.displayIcon()).toBe('');
- expect(component.displayLabel()).toBe('J');
- });
-});
-```
-
-## ๐ฏ Service Testing Patterns
-
-### Signal-Based Service Testing
-
-```typescript
-// Example: user.service.spec.ts
-import { TestBed } from '@angular/core/testing';
-import { UserService } from './user.service';
-
-describe('UserService', () => {
- let service: UserService;
-
- beforeEach(() => {
- TestBed.configureTestingModule({});
- service = TestBed.inject(UserService);
- });
-
- it('should initialize with unauthenticated state', () => {
- expect(service.authenticated()).toBe(false);
- expect(service.user()).toBeNull();
- });
-
- it('should update authentication state', () => {
- const mockUser = {
- sub: '123',
- name: 'John Doe',
- email: 'john@example.com',
- // ... other user properties
- };
-
- service.authenticated.set(true);
- service.user.set(mockUser);
-
- expect(service.authenticated()).toBe(true);
- expect(service.user()).toEqual(mockUser);
- });
-});
-```
-
-### HTTP Service Testing
-
-```typescript
-// Example: project.service.spec.ts
-import { TestBed } from '@angular/core/testing';
-import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
-import { ProjectService } from './project.service';
-
-describe('ProjectService', () => {
- let service: ProjectService;
- let httpMock: HttpTestingController;
-
- beforeEach(() => {
- TestBed.configureTestingModule({
- imports: [HttpClientTestingModule],
- providers: [ProjectService],
- });
-
- service = TestBed.inject(ProjectService);
- httpMock = TestBed.inject(HttpTestingController);
- });
-
- afterEach(() => {
- httpMock.verify();
- });
-
- it('should fetch projects', () => {
- const mockProjects = [
- { id: '1', name: 'Project 1', description: 'Description 1' },
- { id: '2', name: 'Project 2', description: 'Description 2' },
- ];
-
- service.loadProjects();
-
- const req = httpMock.expectOne('/api/projects');
- expect(req.request.method).toBe('GET');
- req.flush(mockProjects);
-
- expect(service.projects()).toEqual(mockProjects);
- expect(service.loading()).toBe(false);
- });
-
- it('should handle errors', () => {
- service.loadProjects();
-
- const req = httpMock.expectOne('/api/projects');
- req.error(new ErrorEvent('Network error'), { status: 500 });
-
- expect(service.error()).toBeTruthy();
- expect(service.loading()).toBe(false);
- });
-});
-```
-
-## ๐ง Testing Configuration
-
-### Jest Configuration
-
-```typescript
-// jest.config.ts
-import type { Config } from 'jest';
-
-const config: Config = {
- preset: 'jest-preset-angular',
- setupFilesAfterEnv: ['/setup-jest.ts'],
- globalSetup: 'jest-preset-angular/global-setup',
- testMatch: ['/src/**/*.spec.ts'],
- collectCoverageFrom: ['/src/**/*.ts', '!/src/**/*.spec.ts', '!/src/main.ts', '!/src/polyfills.ts'],
- coverageDirectory: 'coverage',
- coverageReporters: ['html', 'text-summary', 'lcov'],
- transform: {
- '^.+\\.(ts|mjs|js|html)$': [
- 'jest-preset-angular',
- {
- tsconfig: 'tsconfig.spec.json',
- stringifyContentPathRegex: '\\.(html|svg)$',
- },
- ],
- },
-};
-
-export default config;
-```
-
-### Test Setup
-
-```typescript
-// setup-jest.ts
-import 'jest-preset-angular/setup-jest';
-import '@testing-library/jest-dom';
-
-// Mock global objects
-Object.defineProperty(window, 'CSS', { value: null });
-Object.defineProperty(window, 'getComputedStyle', {
- value: () => ({
- appearance: ['textfield'],
- getPropertyValue: () => '',
- }),
-});
-
-// Mock ResizeObserver
-global.ResizeObserver = class ResizeObserver {
- observe() {}
- unobserve() {}
- disconnect() {}
-};
-```
-
-## ๐ Testing Utilities
-
-### Custom Testing Utilities
-
-```typescript
-// testing/component-helpers.ts
-import { ComponentFixture } from '@angular/core/testing';
-import { DebugElement } from '@angular/core';
-import { By } from '@angular/platform-browser';
-
-export class ComponentHelper {
- constructor(private fixture: ComponentFixture) {}
-
- get component(): T {
- return this.fixture.componentInstance;
- }
-
- get element(): HTMLElement {
- return this.fixture.nativeElement;
- }
-
- detectChanges(): void {
- this.fixture.detectChanges();
- }
-
- querySelector(selector: string): HTMLElement | null {
- return this.element.querySelector(selector);
- }
-
- querySelectorAll(selector: string): NodeListOf {
- return this.element.querySelectorAll(selector);
- }
-
- getByTestId(testId: string): HTMLElement | null {
- return this.querySelector(`[data-testid="${testId}"]`);
- }
-
- clickElement(selector: string): void {
- const element = this.querySelector(selector);
- if (element) {
- element.click();
- this.detectChanges();
- }
- }
-
- setInputValue(selector: string, value: string): void {
- const input = this.querySelector(selector) as HTMLInputElement;
- if (input) {
- input.value = value;
- input.dispatchEvent(new Event('input'));
- this.detectChanges();
- }
- }
-}
-
-// Usage in tests
-export function createComponentHelper(fixture: ComponentFixture): ComponentHelper {
- return new ComponentHelper(fixture);
-}
-```
-
-## ๐ฏ Testing Best Practices
-
-### Component Testing Guidelines
-
-1. **Test User Interactions**: Focus on what users can see and do
-2. **Avoid Implementation Details**: Test behavior, not internal structure
-3. **Use Data Test IDs**: Reliable element selection
-4. **Mock External Dependencies**: Isolate component logic
-5. **Test Signal Updates**: Verify reactive behavior
-
-### Service Testing Guidelines
-
-1. **Test Public API**: Focus on public methods and properties
-2. **Mock HTTP Calls**: Use HttpClientTestingModule
-3. **Test Error Handling**: Verify error states and recovery
-4. **Test Signal State**: Verify state management
-5. **Isolate Dependencies**: Mock external services
-
-### Signal Testing Patterns
-
-```typescript
-// Testing computed signals
-it('should compute filtered data', () => {
- service.data.set([
- { id: 1, active: true },
- { id: 2, active: false },
- { id: 3, active: true },
- ]);
-
- expect(service.activeData()).toEqual([
- { id: 1, active: true },
- { id: 3, active: true },
- ]);
-});
-
-// Testing signal effects
-it('should trigger effect on signal change', () => {
- const effectSpy = jasmine.createSpy('effect');
-
- effect(() => {
- service.data();
- effectSpy();
- });
-
- service.data.set([{ id: 1 }]);
-
- expect(effectSpy).toHaveBeenCalledTimes(2); // Initial + update
-});
-```
-
-## ๐ Implementation Plan
-
-### Phase 1: Enable Testing
-
-1. Remove `skipTests: true` from angular.json
-2. Configure Jest for Angular
-3. Set up testing utilities
-4. Create test examples
-
-### Phase 2: Core Component Tests
-
-1. Test PrimeNG wrapper components
-2. Test layout components
-3. Test page components
-4. Achieve 80%+ component coverage
-
-### Phase 3: Service Tests
-
-1. Test signal-based services
-2. Test HTTP services
-3. Test utility functions
-4. Achieve 90%+ service coverage
-
-### Phase 4: Integration Tests
-
-1. Test component-service integration
-2. Test routing and navigation
-3. Test form workflows
-4. End-to-end critical paths
-
-This testing strategy will ensure high code quality and reliability once implemented in the development workflow.
diff --git a/package.json b/package.json
index 69fbfc13..1fd76cfc 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,9 @@
"start": "turbo run start",
"lint": "turbo run lint",
"test": "turbo run test",
+ "e2e": "turbo run e2e",
+ "e2e:ui": "turbo run e2e:ui",
+ "e2e:headed": "turbo run e2e:headed",
"watch": "turbo run watch",
"start:server": "turbo run start:server",
"start:prod": "pm2 start ecosystem.config.js",
diff --git a/turbo.json b/turbo.json
index d7bfa5e5..2befab72 100644
--- a/turbo.json
+++ b/turbo.json
@@ -32,6 +32,23 @@
"start:server": {
"dependsOn": ["build"],
"cache": false
+ },
+ "e2e": {
+ "dependsOn": ["build"],
+ "inputs": ["$TURBO_DEFAULT$", ".env*", "e2e/**", "playwright.config.ts"],
+ "outputs": ["playwright-report/**", "test-results/**"],
+ "cache": false
+ },
+ "e2e:ui": {
+ "dependsOn": ["build"],
+ "cache": false,
+ "persistent": true
+ },
+ "e2e:headed": {
+ "dependsOn": ["build"],
+ "inputs": ["$TURBO_DEFAULT$", ".env*", "e2e/**", "playwright.config.ts"],
+ "outputs": ["playwright-report/**", "test-results/**"],
+ "cache": false
}
}
}
diff --git a/yarn.lock b/yarn.lock
index ccfb2aa6..9ee56585 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3260,6 +3260,17 @@ __metadata:
languageName: node
linkType: hard
+"@playwright/test@npm:^1.54.1":
+ version: 1.54.1
+ resolution: "@playwright/test@npm:1.54.1"
+ dependencies:
+ playwright: "npm:1.54.1"
+ bin:
+ playwright: cli.js
+ checksum: 10c0/1b414356bc1049927d7b9efc14d5b3bf000ef6483313926bb795b4f27fe3707e8e0acf0db59063a452bb4f7e34559758d17640401b6f3e2f5290f299a8d8d02f
+ languageName: node
+ linkType: hard
+
"@primeng/themes@npm:^19.1.3":
version: 19.1.3
resolution: "@primeng/themes@npm:19.1.3"
@@ -6722,6 +6733,16 @@ __metadata:
languageName: node
linkType: hard
+"fsevents@npm:2.3.2":
+ version: 2.3.2
+ resolution: "fsevents@npm:2.3.2"
+ dependencies:
+ node-gyp: "npm:latest"
+ checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b
+ conditions: os=darwin
+ languageName: node
+ linkType: hard
+
"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
@@ -6732,6 +6753,15 @@ __metadata:
languageName: node
linkType: hard
+"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin":
+ version: 2.3.2
+ resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1"
+ dependencies:
+ node-gyp: "npm:latest"
+ conditions: os=darwin
+ languageName: node
+ linkType: hard
+
"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin":
version: 2.3.3
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1"
@@ -8083,6 +8113,7 @@ __metadata:
"@fullcalendar/timegrid": "npm:^6.1.18"
"@lfx-pcc/shared": "workspace:*"
"@linuxfoundation/lfx-ui-core": "npm:^0.0.19"
+ "@playwright/test": "npm:^1.54.1"
"@primeng/themes": "npm:^19.1.3"
"@types/express": "npm:^4.17.17"
"@types/node": "npm:^18.18.0"
@@ -8099,6 +8130,7 @@ __metadata:
express-openid-connect: "npm:^2.18.1"
ngx-cookie-service-ssr: "npm:^19.1.2"
pino-http: "npm:^10.5.0"
+ playwright: "npm:^1.54.1"
postcss: "npm:^8.5.6"
prettier: "npm:^3.6.2"
prettier-plugin-organize-imports: "npm:^4.1.0"
@@ -9605,6 +9637,30 @@ __metadata:
languageName: node
linkType: hard
+"playwright-core@npm:1.54.1":
+ version: 1.54.1
+ resolution: "playwright-core@npm:1.54.1"
+ bin:
+ playwright-core: cli.js
+ checksum: 10c0/b821262b024d7753b1bfa71eb2bc99f2dda12a869d175b2e1bc6ac2764bd661baf36d9d42f45caf622854ad7e4a6077b9b57014c74bb5a78fe339c9edf1c9019
+ languageName: node
+ linkType: hard
+
+"playwright@npm:1.54.1, playwright@npm:^1.54.1":
+ version: 1.54.1
+ resolution: "playwright@npm:1.54.1"
+ dependencies:
+ fsevents: "npm:2.3.2"
+ playwright-core: "npm:1.54.1"
+ dependenciesMeta:
+ fsevents:
+ optional: true
+ bin:
+ playwright: cli.js
+ checksum: 10c0/c5fedae31a03a1f4c4846569aef3ffb98da23000a4d255abfc8c2ede15b43cc7cd87b80f6fa078666c030373de8103787cf77ef7653ae9458aabbbd4320c2599
+ languageName: node
+ linkType: hard
+
"possible-typed-array-names@npm:^1.0.0":
version: 1.1.0
resolution: "possible-typed-array-names@npm:1.1.0"