Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/src/frames/population/HouseholdBuilderFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import {
Divider,
Group,
Expand Down Expand Up @@ -293,7 +293,7 @@ export default function HouseholdBuilderFrame({
}
});
return options;
});
}, shallowEqual);

const handleSubmit = async () => {
// Sync final household to Redux before submit
Expand Down
85 changes: 82 additions & 3 deletions app/src/tests/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@
import { render, screen, userEvent } from '@test-utils';
```

2. **File location**: Place tests in `src/tests/` mirroring the source structure
- `src/components/Button.tsx` → `src/tests/components/Button.test.tsx`
- `src/hooks/useAuth.ts` → `src/tests/hooks/useAuth.test.ts`
2. **File location**: Place tests in `src/tests/TYPE/` mirroring the source structure, where TYPE is:
- `unit` for unit tests (most common)
- `integration` for integration tests
- Other types as needed

Examples:
- `src/components/Button.tsx` → `src/tests/unit/components/Button.test.tsx`
- `src/hooks/useAuth.ts` → `src/tests/unit/hooks/useAuth.test.ts`
- `src/adapters/HouseholdAdapter.ts` → `src/tests/unit/adapters/HouseholdAdapter.test.ts`
- API integration test → `src/tests/integration/api/PolicyEngine.test.ts`

3. **Test naming**: Use Given-When-Then pattern for clear, descriptive test names
```typescript
Expand Down Expand Up @@ -74,6 +81,63 @@
└── hooks/
└── useAuthMocks.ts
```

**Fixture best practices**:
- **Use descriptive constants**: Define constants that explain their purpose
```typescript
// Good - Clear what each ID represents
export const EXISTING_HOUSEHOLD_ID = '12345';
export const NON_EXISTENT_HOUSEHOLD_ID = '99999';
export const NEW_HOUSEHOLD_ID = 'household-123';

// Bad - Magic numbers/strings in tests
const householdId = '12345'; // What does this represent?
```

- **Group related constants**: Organize constants by their domain
```typescript
export const TEST_COUNTRIES = {
US: 'us',
UK: 'uk',
CA: 'ca',
} as const;

export const HTTP_STATUS = {
OK: 200,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
} as const;
```

- **Create helper functions for common patterns**:
```typescript
export const mockSuccessResponse = (data: any) => ({
ok: true,
status: HTTP_STATUS.OK,
json: vi.fn().mockResolvedValue(data),
});

export const mockErrorResponse = (status: number) => ({
ok: false,
status,
statusText: status === HTTP_STATUS.NOT_FOUND ? 'Not Found' : 'Error',
});
```

- **Match implementation error messages**: Keep error constants in sync
```typescript
export const ERROR_MESSAGES = {
FETCH_FAILED: (id: string) => `Failed to fetch household ${id}`,
CREATE_FAILED: 'Failed to create household',
} as const;
```

- **Provide variants for different test scenarios**:
```typescript
export const mockHouseholdPayload = { /* base payload */ };
export const mockHouseholdPayloadUK = { ...mockHouseholdPayload, country_id: 'uk' };
export const mockLargeHouseholdPayload = { /* complex payload with all fields */ };
```

7. **Always mock Plotly**: Add to test file or setup
```typescript
Expand All @@ -85,11 +149,26 @@
- Mock API calls at module level
- Reset mocks with `vi.clearAllMocks()` in `beforeEach`
- Import mocks from fixtures: `import { mockPolicyData } from '@/tests/fixtures/api/policyMocks';`
- Keep test data in fixtures, not inline in tests (improves maintainability)

9. **Async patterns**: Use `userEvent.setup()` for interactions, `waitFor` for async updates

10. **Coverage targets**: Aim for 80% overall, 90% for critical paths (adapters, utils, hooks)

11. **Test readability principles**:
- **Use constants over magic values**: Makes tests self-documenting
```typescript
// Good - Intent is clear
const result = await fetchHouseholdById(TEST_COUNTRIES.US, EXISTING_HOUSEHOLD_ID);

// Bad - What do these values represent?
const result = await fetchHouseholdById('us', '12345');
```

- **One assertion per test when possible**: Makes failures easier to diagnose
- **Test the "what", not the "how"**: Focus on behavior, not implementation
- **Use fixture data that represents real scenarios**: Don't use "foo", "bar", "test123"

## What to test
- User interactions and state changes
- Props rendering correctly
Expand Down
153 changes: 153 additions & 0 deletions app/src/tests/fixtures/adapters/HouseholdAdapterMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { HouseholdData } from '@/types/ingredients/Household';
import { HouseholdMetadata } from '@/types/metadata/householdMetadata';

export const mockEntityMetadata = {
person: {
key: 'person',
plural: 'people',
label: 'Person',
},
tax_unit: {
key: 'tax_unit',
plural: 'tax_units',
label: 'Tax unit',
},
marital_unit: {
key: 'marital_unit',
plural: 'marital_units',
label: 'Marital unit',
},
household: {
key: 'household',
plural: 'households',
label: 'Household',
},
spm_unit: {
key: 'spm_unit',
plural: 'spm_units',
label: 'SPM unit',
},
};

export const mockHouseholdMetadata: HouseholdMetadata = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question, non-blocking: Why is this mock duplicated (with slightly reordered children) in both api and adapter fixtures?

id: '12345',
country_id: 'us',
household_json: {
people: {
person1: {
age: { 2024: 30 },
employment_income: { 2024: 50000 },
},
person2: {
age: { 2024: 28 },
employment_income: { 2024: 45000 },
},
},
tax_units: {
tax_unit1: {
members: ['person1', 'person2'],
},
},
marital_units: {
marital_unit1: {
members: ['person1', 'person2'],
},
},
spm_units: {
spm_unit1: {
members: ['person1', 'person2'],
},
},
households: {
household1: {
members: ['person1', 'person2'],
},
},
families: {
family1: {
members: ['person1', 'person2'],
},
},
},
api_version: 'v1',
household_hash: '<household_hash>',
};

export const mockHouseholdMetadataWithUnknownEntity: HouseholdMetadata = {
id: '67890',
country_id: 'uk',
household_json: {
people: {
person1: {
age: { 2024: 40 },
},
},
// @ts-expect-error
unknown_entity: {
entity1: {
some_property: 'value',
},
},
},
};

export const mockHouseholdData: HouseholdData = {
people: {
person1: {
age: { 2024: 30 },
employment_income: { 2024: 50000 },
},
person2: {
age: { 2024: 28 },
employment_income: { 2024: 45000 },
},
},
taxUnits: {
tax_unit1: {
members: ['person1', 'person2'],
head: 'person1',
},
},
maritalUnits: {
marital_unit1: {
members: ['person1', 'person2'],
},
},
};

export const mockHouseholdDataWithMultipleEntities: HouseholdData = {
people: {
person1: { age: { 2024: 25 } },
person2: { age: { 2024: 23 } },
person3: { age: { 2024: 5 } },
},
taxUnits: {
tax_unit1: {
members: ['person1', 'person2', 'person3'],
head: 'person1',
},
},
maritalUnits: {
marital_unit1: {
members: ['person1', 'person2'],
},
},
spmUnits: {
spm_unit1: {
members: ['person1', 'person2', 'person3'],
},
},
};

export const mockEmptyHouseholdData: HouseholdData = {
people: {},
};

export const mockHouseholdDataWithUnknownEntity: HouseholdData = {
people: {
person1: { age: { 2024: 30 } },
},
customEntity: {
entity1: { custom_field: 'value' },
},
} as any;
77 changes: 77 additions & 0 deletions app/src/tests/fixtures/api/householdAssociationMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { UserHouseholdPopulation } from '@/types/ingredients/UserPopulation';

export const mockUserHouseholdPopulation: UserHouseholdPopulation = {
type: 'household',
id: 'household-123',
householdId: 'household-123',
userId: 'user-456',
label: 'My Test Household',
createdAt: '2024-01-15T10:00:00Z',
updatedAt: '2024-01-15T10:00:00Z',
isCreated: true,
} as any;

export const mockUserHouseholdPopulationList: UserHouseholdPopulation[] = [
{
type: 'household',
id: 'household-1',
householdId: 'household-1',
userId: 'user-456',
label: 'First Household',
createdAt: '2024-01-10T10:00:00Z',
updatedAt: '2024-01-10T10:00:00Z',
isCreated: true,
} as any,
{
type: 'household',
id: 'household-2',
householdId: 'household-2',
userId: 'user-456',
label: 'Second Household',
createdAt: '2024-01-12T10:00:00Z',
updatedAt: '2024-01-12T10:00:00Z',
isCreated: true,
} as any,
];

export const mockApiResponse = {
id: 'household-123',
household_id: 'household-123',
user_id: 'user-456',
user_label: 'My Test Household',
country_id: 'us',
created_at: '2024-01-15T10:00:00Z',
updated_at: '2024-01-15T10:00:00Z',
is_default: false,
};

export const mockApiResponseList = [
{
id: 'household-1',
household_id: 'household-1',
user_id: 'user-456',
user_label: 'First Household',
country_id: 'us',
created_at: '2024-01-10T10:00:00Z',
updated_at: '2024-01-10T10:00:00Z',
is_default: true,
},
{
id: 'household-2',
household_id: 'household-2',
user_id: 'user-456',
user_label: 'Second Household',
country_id: 'uk',
created_at: '2024-01-12T10:00:00Z',
updated_at: '2024-01-12T10:00:00Z',
is_default: false,
},
];

export const mockCreationPayload = {
household_id: 'household-123',
user_id: 'user-456',
user_label: 'My Test Household',
country_id: 'us',
is_default: false,
};
Loading
Loading