Implement Phase 8: Testing & Deployment
Testing Infrastructure: - Configure Jest with Next.js integration and TypeScript support - Add React Testing Library with proper setup for Next.js components - Install and configure Playwright for E2E testing - Create jest.setup.ts with mocks for Next.js router and browser APIs - Add CSS module mocking with identity-obj-proxy Unit Tests: - Create LoadingFallback component tests covering all 5 variants - Create tracking API tests for data transformation logic - Test createActivity, getActivities data structure conversions - Verify frontend (timestamp/data) to backend (startedAt/metadata) mapping E2E Tests (Playwright): - Create comprehensive tracking flow tests - Test navigation to all tracker pages (feeding, sleep, diaper) - Test homepage Today's Summary display - Test AI Assistant and Analytics navigation - Test feeding, sleep, and diaper tracker UI elements - Configure multi-browser testing (Chrome, Firefox, Safari, Mobile) CI/CD Pipeline (GitHub Actions): - Create automated CI/CD workflow for master/main branches - Run linting on every push and PR - Execute unit tests with coverage reporting - Run E2E tests (Chromium) in CI environment - Build application and upload artifacts - Upload test coverage to Codecov - Upload Playwright test reports as artifacts Test Scripts: - npm test: Run Jest unit tests - npm run test⌚ Run tests in watch mode - npm run test:coverage: Generate coverage report - npm run test:e2e: Run Playwright E2E tests - npm run test:e2e:ui: Run E2E tests with UI mode - npm run test:e2e:headed: Run E2E tests in headed mode Documentation: - Create comprehensive testing guide (tests/README.md) - Document test structure and best practices - Add troubleshooting section for common issues - Include useful commands for debugging tests Coverage Thresholds: - Branches: 70% - Functions: 70% - Lines: 70% - Statements: 70% 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
115
.github/workflows/ci.yml
vendored
Normal file
115
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
name: CI/CD Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, main]
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-test:
|
||||||
|
name: Lint and Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: maternal-web
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: maternal-web/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: npm run test -- --ci --coverage
|
||||||
|
|
||||||
|
- name: Upload coverage reports
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
directory: maternal-web/coverage
|
||||||
|
flags: frontend
|
||||||
|
fail_ci_if_error: false
|
||||||
|
|
||||||
|
e2e-tests:
|
||||||
|
name: E2E Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint-and-test
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: maternal-web
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: maternal-web/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Run E2E tests
|
||||||
|
run: npm run test:e2e -- --project=chromium
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: maternal-web/playwright-report/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build Application
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint-and-test
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: maternal-web
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: maternal-web/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: maternal-web/.next/
|
||||||
|
retention-days: 7
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { LoadingFallback } from '../LoadingFallback'
|
||||||
|
|
||||||
|
describe('LoadingFallback', () => {
|
||||||
|
it('renders without crashing for page variant', () => {
|
||||||
|
const { container } = render(<LoadingFallback variant="page" />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders without crashing for card variant', () => {
|
||||||
|
const { container } = render(<LoadingFallback variant="card" />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders without crashing for list variant', () => {
|
||||||
|
const { container } = render(<LoadingFallback variant="list" />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders without crashing for chart variant', () => {
|
||||||
|
const { container } = render(<LoadingFallback variant="chart" />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders without crashing for chat variant', () => {
|
||||||
|
const { container } = render(<LoadingFallback variant="chat" />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults to page variant when no variant is specified', () => {
|
||||||
|
const { container } = render(<LoadingFallback />)
|
||||||
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,10 +7,11 @@ const createJestConfig = nextJest({
|
|||||||
|
|
||||||
// Add any custom config to be passed to Jest
|
// Add any custom config to be passed to Jest
|
||||||
const customJestConfig = {
|
const customJestConfig = {
|
||||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||||
testEnvironment: 'jest-environment-jsdom',
|
testEnvironment: 'jest-environment-jsdom',
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^@/(.*)$': '<rootDir>/$1',
|
'^@/(.*)$': '<rootDir>/$1',
|
||||||
|
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
||||||
},
|
},
|
||||||
collectCoverageFrom: [
|
collectCoverageFrom: [
|
||||||
'app/**/*.{js,jsx,ts,tsx}',
|
'app/**/*.{js,jsx,ts,tsx}',
|
||||||
|
|||||||
42
maternal-web/jest.setup.ts
Normal file
42
maternal-web/jest.setup.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import '@testing-library/jest-dom'
|
||||||
|
|
||||||
|
// Mock Next.js router
|
||||||
|
jest.mock('next/navigation', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: jest.fn(),
|
||||||
|
replace: jest.fn(),
|
||||||
|
prefetch: jest.fn(),
|
||||||
|
back: jest.fn(),
|
||||||
|
pathname: '/',
|
||||||
|
query: {},
|
||||||
|
asPath: '/',
|
||||||
|
}),
|
||||||
|
usePathname: () => '/',
|
||||||
|
useSearchParams: () => new URLSearchParams(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock window.matchMedia
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: jest.fn().mockImplementation((query) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: jest.fn(),
|
||||||
|
removeListener: jest.fn(),
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
removeEventListener: jest.fn(),
|
||||||
|
dispatchEvent: jest.fn(),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock IntersectionObserver
|
||||||
|
global.IntersectionObserver = class IntersectionObserver {
|
||||||
|
constructor() {}
|
||||||
|
disconnect() {}
|
||||||
|
observe() {}
|
||||||
|
takeRecords() {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
unobserve() {}
|
||||||
|
}
|
||||||
104
maternal-web/lib/api/__tests__/tracking.test.ts
Normal file
104
maternal-web/lib/api/__tests__/tracking.test.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { trackingApi } from '../tracking'
|
||||||
|
import apiClient from '../client'
|
||||||
|
|
||||||
|
jest.mock('../client')
|
||||||
|
|
||||||
|
const mockedApiClient = apiClient as jest.Mocked<typeof apiClient>
|
||||||
|
|
||||||
|
describe('trackingApi', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('createActivity', () => {
|
||||||
|
it('transforms frontend data to backend format', async () => {
|
||||||
|
const mockActivity = {
|
||||||
|
id: 'act_123',
|
||||||
|
childId: 'chd_456',
|
||||||
|
type: 'feeding',
|
||||||
|
startedAt: '2024-01-01T12:00:00Z',
|
||||||
|
metadata: { amount: 120, type: 'bottle' },
|
||||||
|
loggedBy: 'usr_789',
|
||||||
|
createdAt: '2024-01-01T12:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
mockedApiClient.post.mockResolvedValue({
|
||||||
|
data: { data: { activity: mockActivity } },
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const result = await trackingApi.createActivity('chd_456', {
|
||||||
|
type: 'feeding',
|
||||||
|
timestamp: '2024-01-01T12:00:00Z',
|
||||||
|
data: { amount: 120, type: 'bottle' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockedApiClient.post).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/activities?childId=chd_456',
|
||||||
|
{
|
||||||
|
type: 'feeding',
|
||||||
|
startedAt: '2024-01-01T12:00:00Z',
|
||||||
|
metadata: { amount: 120, type: 'bottle' },
|
||||||
|
notes: undefined,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
...mockActivity,
|
||||||
|
timestamp: mockActivity.startedAt,
|
||||||
|
data: mockActivity.metadata,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getActivities', () => {
|
||||||
|
it('transforms backend data to frontend format', async () => {
|
||||||
|
const mockActivities = [
|
||||||
|
{
|
||||||
|
id: 'act_123',
|
||||||
|
childId: 'chd_456',
|
||||||
|
type: 'feeding',
|
||||||
|
startedAt: '2024-01-01T12:00:00Z',
|
||||||
|
metadata: { amount: 120 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'act_124',
|
||||||
|
childId: 'chd_456',
|
||||||
|
type: 'sleep',
|
||||||
|
startedAt: '2024-01-01T14:00:00Z',
|
||||||
|
metadata: { duration: 120 },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
mockedApiClient.get.mockResolvedValue({
|
||||||
|
data: { data: { activities: mockActivities } },
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const result = await trackingApi.getActivities('chd_456', 'feeding')
|
||||||
|
|
||||||
|
expect(mockedApiClient.get).toHaveBeenCalledWith('/api/v1/activities', {
|
||||||
|
params: { childId: 'chd_456', type: 'feeding' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: 'act_123',
|
||||||
|
childId: 'chd_456',
|
||||||
|
type: 'feeding',
|
||||||
|
startedAt: '2024-01-01T12:00:00Z',
|
||||||
|
metadata: { amount: 120 },
|
||||||
|
timestamp: '2024-01-01T12:00:00Z',
|
||||||
|
data: { amount: 120 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'act_124',
|
||||||
|
childId: 'chd_456',
|
||||||
|
type: 'sleep',
|
||||||
|
startedAt: '2024-01-01T14:00:00Z',
|
||||||
|
metadata: { duration: 120 },
|
||||||
|
timestamp: '2024-01-01T14:00:00Z',
|
||||||
|
data: { duration: 120 },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
258
maternal-web/package-lock.json
generated
258
maternal-web/package-lock.json
generated
@@ -35,6 +35,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/react": "^4.10.2",
|
"@axe-core/react": "^4.10.2",
|
||||||
|
"@playwright/test": "^1.55.1",
|
||||||
"@testing-library/jest-dom": "^6.9.0",
|
"@testing-library/jest-dom": "^6.9.0",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
@@ -42,11 +43,13 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"jest-axe": "^10.0.0",
|
"jest-axe": "^10.0.0",
|
||||||
"jest-environment-jsdom": "^30.2.0",
|
"jest-environment-jsdom": "^30.2.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"ts-jest": "^29.4.4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3154,6 +3157,22 @@
|
|||||||
"url": "https://opencollective.com/pkgr"
|
"url": "https://opencollective.com/pkgr"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.55.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz",
|
||||||
|
"integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.55.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@popperjs/core": {
|
"node_modules/@popperjs/core": {
|
||||||
"version": "2.11.8",
|
"version": "2.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
@@ -4922,6 +4941,19 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bs-logger": {
|
||||||
|
"version": "0.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
|
||||||
|
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-json-stable-stringify": "2.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bser": {
|
"node_modules/bser": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
|
||||||
@@ -6982,6 +7014,45 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/handlebars": {
|
||||||
|
"version": "4.7.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||||
|
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"neo-async": "^2.6.2",
|
||||||
|
"source-map": "^0.6.1",
|
||||||
|
"wordwrap": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"handlebars": "bin/handlebars"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.7"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"uglify-js": "^3.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/handlebars/node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/harmony-reflect": {
|
||||||
|
"version": "1.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz",
|
||||||
|
"integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(Apache-2.0 OR MPL-1.1)"
|
||||||
|
},
|
||||||
"node_modules/has-bigints": {
|
"node_modules/has-bigints": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
||||||
@@ -7161,6 +7232,19 @@
|
|||||||
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
|
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/identity-obj-proxy": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"harmony-reflect": "^1.4.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -9350,6 +9434,13 @@
|
|||||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.memoize": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -9417,6 +9508,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/makeerror": {
|
"node_modules/makeerror": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
|
||||||
@@ -9521,6 +9619,16 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
@@ -9609,8 +9717,7 @@
|
|||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "14.2.0",
|
"version": "14.2.0",
|
||||||
@@ -10439,6 +10546,53 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.55.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
|
||||||
|
"integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.55.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.55.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
|
||||||
|
"integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
@@ -12470,6 +12624,85 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-jest": {
|
||||||
|
"version": "29.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.4.tgz",
|
||||||
|
"integrity": "sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bs-logger": "^0.2.6",
|
||||||
|
"fast-json-stable-stringify": "^2.1.0",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
|
"json5": "^2.2.3",
|
||||||
|
"lodash.memoize": "^4.1.2",
|
||||||
|
"make-error": "^1.3.6",
|
||||||
|
"semver": "^7.7.2",
|
||||||
|
"type-fest": "^4.41.0",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-jest": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": ">=7.0.0-beta.0 <8",
|
||||||
|
"@jest/transform": "^29.0.0 || ^30.0.0",
|
||||||
|
"@jest/types": "^29.0.0 || ^30.0.0",
|
||||||
|
"babel-jest": "^29.0.0 || ^30.0.0",
|
||||||
|
"jest": "^29.0.0 || ^30.0.0",
|
||||||
|
"jest-util": "^29.0.0 || ^30.0.0",
|
||||||
|
"typescript": ">=4.3 <6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@babel/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@jest/transform": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@jest/types": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"babel-jest": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jest-util": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-jest/node_modules/semver": {
|
||||||
|
"version": "7.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-jest/node_modules/type-fest": {
|
||||||
|
"version": "4.41.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||||
|
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
@@ -12586,6 +12819,20 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uglify-js": {
|
||||||
|
"version": "3.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||||
|
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"uglifyjs": "bin/uglifyjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
@@ -13115,6 +13362,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wordwrap": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/workbox-background-sync": {
|
"node_modules/workbox-background-sync": {
|
||||||
"version": "7.3.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz",
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:coverage": "jest --coverage"
|
"test:coverage": "jest --coverage",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:headed": "playwright test --headed"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
@@ -39,6 +42,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/react": "^4.10.2",
|
"@axe-core/react": "^4.10.2",
|
||||||
|
"@playwright/test": "^1.55.1",
|
||||||
"@testing-library/jest-dom": "^6.9.0",
|
"@testing-library/jest-dom": "^6.9.0",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
@@ -46,11 +50,13 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"jest-axe": "^10.0.0",
|
"jest-axe": "^10.0.0",
|
||||||
"jest-environment-jsdom": "^30.2.0",
|
"jest-environment-jsdom": "^30.2.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"ts-jest": "^29.4.4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
maternal-web/playwright.config.ts
Normal file
46
maternal-web/playwright.config.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3030',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
// Mobile viewports
|
||||||
|
{
|
||||||
|
name: 'Mobile Chrome',
|
||||||
|
use: { ...devices['Pixel 5'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mobile Safari',
|
||||||
|
use: { ...devices['iPhone 12'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:3030',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 120000,
|
||||||
|
},
|
||||||
|
});
|
||||||
145
maternal-web/tests/README.md
Normal file
145
maternal-web/tests/README.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Testing Guide
|
||||||
|
|
||||||
|
This document describes the testing setup and best practices for the Maternal Web application.
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
maternal-web/
|
||||||
|
├── components/
|
||||||
|
│ └── **/__tests__/ # Component unit tests
|
||||||
|
├── lib/
|
||||||
|
│ └── **/__tests__/ # Library/utility unit tests
|
||||||
|
├── tests/
|
||||||
|
│ └── e2e/ # End-to-end tests
|
||||||
|
├── jest.config.js # Jest configuration
|
||||||
|
├── jest.setup.ts # Jest setup file
|
||||||
|
└── playwright.config.ts # Playwright configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### Unit Tests (Jest + React Testing Library)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all unit tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run tests in watch mode
|
||||||
|
npm run test:watch
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
npm run test:coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### E2E Tests (Playwright)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all E2E tests
|
||||||
|
npm run test:e2e
|
||||||
|
|
||||||
|
# Run E2E tests with UI
|
||||||
|
npm run test:e2e:ui
|
||||||
|
|
||||||
|
# Run E2E tests in headed mode (see browser)
|
||||||
|
npm run test:e2e:headed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Tests
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
Unit tests should be placed in `__tests__` directories next to the code they test.
|
||||||
|
|
||||||
|
Example component test:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { MyComponent } from '../MyComponent'
|
||||||
|
|
||||||
|
describe('MyComponent', () => {
|
||||||
|
it('renders correctly', () => {
|
||||||
|
render(<MyComponent title="Test" />)
|
||||||
|
expect(screen.getByText('Test')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### E2E Tests
|
||||||
|
|
||||||
|
E2E tests should be placed in `tests/e2e/` directory.
|
||||||
|
|
||||||
|
Example E2E test:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('should navigate to page', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.locator('h1')).toContainText('Welcome');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage Thresholds
|
||||||
|
|
||||||
|
The project maintains the following coverage thresholds:
|
||||||
|
|
||||||
|
- Branches: 70%
|
||||||
|
- Functions: 70%
|
||||||
|
- Lines: 70%
|
||||||
|
- Statements: 70%
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
Tests run automatically on:
|
||||||
|
- Every push to `master` or `main` branches
|
||||||
|
- Every pull request
|
||||||
|
|
||||||
|
The CI pipeline:
|
||||||
|
1. Runs linting
|
||||||
|
2. Runs unit tests with coverage
|
||||||
|
3. Runs E2E tests (Chromium only in CI)
|
||||||
|
4. Builds the application
|
||||||
|
5. Uploads test artifacts
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Write tests for new features** - All new features should include tests
|
||||||
|
2. **Test user interactions** - Focus on testing what users see and do
|
||||||
|
3. **Keep tests simple** - Each test should test one thing
|
||||||
|
4. **Use descriptive test names** - Test names should describe what they test
|
||||||
|
5. **Avoid implementation details** - Test behavior, not implementation
|
||||||
|
6. **Mock external dependencies** - Use mocks for API calls and external services
|
||||||
|
|
||||||
|
## Useful Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run specific test file
|
||||||
|
npm test -- MyComponent.test.tsx
|
||||||
|
|
||||||
|
# Run tests matching pattern
|
||||||
|
npm test -- --testNamePattern="should render"
|
||||||
|
|
||||||
|
# Update snapshots
|
||||||
|
npm test -- -u
|
||||||
|
|
||||||
|
# Debug tests
|
||||||
|
node --inspect-brk node_modules/.bin/jest --runInBand
|
||||||
|
|
||||||
|
# Generate Playwright test code
|
||||||
|
npx playwright codegen http://localhost:3030
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Jest
|
||||||
|
|
||||||
|
- **Tests timing out**: Increase timeout with `jest.setTimeout(10000)` in test file
|
||||||
|
- **Module not found**: Check `moduleNameMapper` in `jest.config.js`
|
||||||
|
- **Async tests failing**: Make sure to `await` async operations and use `async/await` in tests
|
||||||
|
|
||||||
|
### Playwright
|
||||||
|
|
||||||
|
- **Browser not launching**: Run `npx playwright install` to install browsers
|
||||||
|
- **Tests flaky**: Add `await page.waitForLoadState('networkidle')` or explicit waits
|
||||||
|
- **Selectors not working**: Use Playwright Inspector with `npx playwright test --debug`
|
||||||
139
maternal-web/tests/e2e/tracking.spec.ts
Normal file
139
maternal-web/tests/e2e/tracking.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Activity Tracking Flow', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Login before each test
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.fill('input[name="email"]', 'andrei@cloudz.ro');
|
||||||
|
await page.fill('input[name="password"]', 'password');
|
||||||
|
await page.click('button[type="submit"]');
|
||||||
|
|
||||||
|
// Wait for redirect to homepage
|
||||||
|
await page.waitForURL('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to feeding tracker', async ({ page }) => {
|
||||||
|
// Click on feeding quick action
|
||||||
|
await page.click('text=Feeding');
|
||||||
|
|
||||||
|
// Verify we're on the feeding page
|
||||||
|
await expect(page).toHaveURL('/track/feeding');
|
||||||
|
await expect(page.locator('text=Track Feeding')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to sleep tracker', async ({ page }) => {
|
||||||
|
// Click on sleep quick action
|
||||||
|
await page.click('text=Sleep');
|
||||||
|
|
||||||
|
// Verify we're on the sleep page
|
||||||
|
await expect(page).toHaveURL('/track/sleep');
|
||||||
|
await expect(page.locator('text=Track Sleep')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to diaper tracker', async ({ page }) => {
|
||||||
|
// Click on diaper quick action
|
||||||
|
await page.click('text=Diaper');
|
||||||
|
|
||||||
|
// Verify we're on the diaper page
|
||||||
|
await expect(page).toHaveURL('/track/diaper');
|
||||||
|
await expect(page.locator('text=Track Diaper Change')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display today summary on homepage', async ({ page }) => {
|
||||||
|
// Check that Today's Summary section exists
|
||||||
|
await expect(page.locator('text=Today\'s Summary')).toBeVisible();
|
||||||
|
|
||||||
|
// Check that the three metrics are displayed
|
||||||
|
await expect(page.locator('text=Feedings')).toBeVisible();
|
||||||
|
await expect(page.locator('text=Sleep')).toBeVisible();
|
||||||
|
await expect(page.locator('text=Diapers')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to AI Assistant', async ({ page }) => {
|
||||||
|
// Click on AI Assistant quick action
|
||||||
|
await page.click('text=AI Assistant');
|
||||||
|
|
||||||
|
// Verify we're on the AI Assistant page
|
||||||
|
await expect(page).toHaveURL('/ai-assistant');
|
||||||
|
await expect(page.locator('text=AI Assistant')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to Analytics', async ({ page }) => {
|
||||||
|
// Click on Analytics quick action
|
||||||
|
await page.click('text=Analytics');
|
||||||
|
|
||||||
|
// Verify we're on the Analytics page
|
||||||
|
await expect(page).toHaveURL('/analytics');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Feeding Tracker', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.fill('input[name="email"]', 'andrei@cloudz.ro');
|
||||||
|
await page.fill('input[name="password"]', 'password');
|
||||||
|
await page.click('button[type="submit"]');
|
||||||
|
await page.waitForURL('/');
|
||||||
|
|
||||||
|
// Navigate to feeding tracker
|
||||||
|
await page.goto('/track/feeding');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have feeding type options', async ({ page }) => {
|
||||||
|
// Check that feeding type buttons are visible
|
||||||
|
await expect(page.locator('text=Bottle')).toBeVisible();
|
||||||
|
await expect(page.locator('text=Breast')).toBeVisible();
|
||||||
|
await expect(page.locator('text=Solid')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have save button', async ({ page }) => {
|
||||||
|
await expect(page.locator('button:has-text("Save")')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Sleep Tracker', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.fill('input[name="email"]', 'andrei@cloudz.ro');
|
||||||
|
await page.fill('input[name="password"]', 'password');
|
||||||
|
await page.click('button[type="submit"]');
|
||||||
|
await page.waitForURL('/');
|
||||||
|
|
||||||
|
// Navigate to sleep tracker
|
||||||
|
await page.goto('/track/sleep');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have sleep type options', async ({ page }) => {
|
||||||
|
// Check that sleep type buttons are visible
|
||||||
|
await expect(page.locator('text=Nap')).toBeVisible();
|
||||||
|
await expect(page.locator('text=Night')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have save button', async ({ page }) => {
|
||||||
|
await expect(page.locator('button:has-text("Save")')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Diaper Tracker', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/login');
|
||||||
|
await page.fill('input[name="email"]', 'andrei@cloudz.ro');
|
||||||
|
await page.fill('input[name="password"]', 'password');
|
||||||
|
await page.click('button[type="submit"]');
|
||||||
|
await page.waitForURL('/');
|
||||||
|
|
||||||
|
// Navigate to diaper tracker
|
||||||
|
await page.goto('/track/diaper');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have diaper type options', async ({ page }) => {
|
||||||
|
// Check that diaper type buttons are visible
|
||||||
|
await expect(page.locator('text=Wet')).toBeVisible();
|
||||||
|
await expect(page.locator('text=Dirty')).toBeVisible();
|
||||||
|
await expect(page.locator('text=Both')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have save button', async ({ page }) => {
|
||||||
|
await expect(page.locator('button:has-text("Save")')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user