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:
@@ -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
|
||||
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}',
|
||||
|
||||
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": {
|
||||
"@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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
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