Implement Phase 8: Testing & Deployment
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

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:
andupetcu
2025-10-01 10:22:53 +03:00
parent 0a2e28b5ee
commit d25febf2a2
10 changed files with 890 additions and 4 deletions

View File

@@ -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()
})
})

View File

@@ -7,10 +7,11 @@ const createJestConfig = nextJest({
// Add any custom config to be passed to Jest
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
collectCoverageFrom: [
'app/**/*.{js,jsx,ts,tsx}',

View 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() {}
}

View 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 },
},
])
})
})
})

View File

@@ -35,6 +35,7 @@
},
"devDependencies": {
"@axe-core/react": "^4.10.2",
"@playwright/test": "^1.55.1",
"@testing-library/jest-dom": "^6.9.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
@@ -42,11 +43,13 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0",
"jest-axe": "^10.0.0",
"jest-environment-jsdom": "^30.2.0",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.4.4",
"typescript": "^5"
}
},
@@ -3154,6 +3157,22 @@
"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": {
"version": "2.11.8",
"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_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": {
"version": "2.1.1",
"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==",
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -7161,6 +7232,19 @@
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
"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": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -9350,6 +9434,13 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"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": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -9417,6 +9508,13 @@
"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": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -9521,6 +9619,16 @@
"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": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -9609,8 +9717,7 @@
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/next": {
"version": "14.2.0",
@@ -10439,6 +10546,53 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -12470,6 +12624,85 @@
"dev": true,
"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": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -12586,6 +12819,20 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -13115,6 +13362,13 @@
"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": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz",

View File

@@ -9,7 +9,10 @@
"lint": "next lint",
"test": "jest",
"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": {
"@emotion/react": "^11.14.0",
@@ -39,6 +42,7 @@
},
"devDependencies": {
"@axe-core/react": "^4.10.2",
"@playwright/test": "^1.55.1",
"@testing-library/jest-dom": "^6.9.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
@@ -46,11 +50,13 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0",
"jest-axe": "^10.0.0",
"jest-environment-jsdom": "^30.2.0",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.4.4",
"typescript": "^5"
}
}

View 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,
},
});

View 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`

View 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();
});
});