This commit is contained in:
jableader 2025-10-18 13:17:39 +11:00
parent f0a8adbd77
commit 661d5f4840
6 changed files with 114 additions and 4 deletions

View file

@ -7,7 +7,10 @@
"build": "vue-cli-service build", "build": "vue-cli-service build",
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint",
"format": "prettier --write .", "format": "prettier --write .",
"prepare": "husky install" "prepare": "husky install",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.8.3", "core-js": "^3.8.3",

View file

@ -55,9 +55,11 @@ Outcome: Stable formatting and consistent linting across contributors.
### Phase 5 — Tests (Targeted) ### Phase 5 — Tests (Targeted)
- [ ] Add a unit test runner (Vitest or Jest) - [x] Add a unit test runner (Vitest)
- [ ] Test mappers (dates/reference wiring) - [x] Test mappers (dates/reference wiring)
- [ ] Test `units.js` conversions and totals - Added tests: `tests/mealMapper.test.js`, `tests/shoppingListMapper.test.js`
- [x] Test `units.js` conversions and totals
- Added tests: `tests/units.test.js`
Outcome: Confidence in refactors and easier onboarding. Outcome: Confidence in refactors and easier onboarding.
@ -80,3 +82,5 @@ Outcome: Confidence in refactors and easier onboarding.
- Added `composables/useMeals.js`; migrated meal pages to composable and `<script setup>` - Added `composables/useMeals.js`; migrated meal pages to composable and `<script setup>`
- Added `composables/useShopping.js`; migrated shopping pages to composable and `<script setup>` - Added `composables/useShopping.js`; migrated shopping pages to composable and `<script setup>`
- Added VS Code Volar recommendation (`.vscode/extensions.json`) - Added VS Code Volar recommendation (`.vscode/extensions.json`)
- Set up Vitest (`vitest.config.js`), added test scripts in `package.json`
- Wrote unit tests for mappers and units: `tests/mealMapper.test.js`, `tests/shoppingListMapper.test.js`, `tests/units.test.js`

27
tests/mealMapper.test.js Normal file
View file

@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest'
import { mapMeal, mapMeals } from '@/api/mappers/mealMapper'
describe('mealMapper', () => {
it('maps individual meal date fields to Date instances', () => {
const input = {
id: 1,
suggested_date: '2025-01-01T00:00:00Z',
purchase_date: '2025-01-02T00:00:00Z',
consumed_date: '2025-01-03T00:00:00Z',
}
const result = mapMeal({ ...input })
expect(result.suggested_date).toBeInstanceOf(Date)
expect(result.purchase_date).toBeInstanceOf(Date)
expect(result.consumed_date).toBeInstanceOf(Date)
})
it('maps lists of meals', () => {
const input = [
{ id: 1, suggested_date: '2025-01-01T00:00:00Z' },
{ id: 2, suggested_date: '2025-01-02T00:00:00Z' },
]
const result = mapMeals(input)
expect(result).toHaveLength(2)
expect(result[0].suggested_date).toBeInstanceOf(Date)
})
})

View file

@ -0,0 +1,33 @@
import { describe, it, expect } from 'vitest'
import { mapCurrentShoppingList, mapPurchasedShoppingList } from '@/api/mappers/shoppingListMapper'
describe('shoppingListMapper', () => {
it('maps current shopping list and wires references', () => {
const dto = {
ingredients_lookup: { 10: { id: 10, name: 'Eggs' } },
meals_lookup: { 1: { id: 1, suggested_date: '2025-01-01T00:00:00Z', recipes: [], extra_ingredients: [], chefs: [], consumers: [], cleanup: [] } },
recipes_lookup: { 5: { id: 5, name: 'Omelette' } },
shopping_list_lookup: { 7: { id: 7, created_date: '2025-01-01T00:00:00Z' } },
outstanding_items: [{ ingredient_id: 10, meal_id: 1, recipe_id: 5, list_id: 7, created_date: '2025-01-01T00:00:00Z' }],
requested_meals: [],
purchased_items: [],
}
const mapped = mapCurrentShoppingList(dto)
const item = mapped.outstanding_items[0]
expect(item.ingredient.name).toBe('Eggs')
expect(item.meal.suggested_date).toBeInstanceOf(Date)
expect(mapped.shopping_list_lookup['7'].created_date).toBeInstanceOf(Date)
})
it('maps purchased shopping list with list dates and item refs', () => {
const dto = {
ingredients_lookup: { 10: { id: 10, name: 'Eggs' } },
meals_lookup: { 1: { id: 1, suggested_date: '2025-01-01T00:00:00Z', recipes: [], extra_ingredients: [], chefs: [], consumers: [], cleanup: [] } },
recipes_lookup: { 5: { id: 5, name: 'Omelette' } },
list: { id: 9, created_date: '2025-02-02T00:00:00Z', items: [{ ingredient_id: 10, meal_id: 1, recipe_id: 5, list_id: 9 }] },
}
const mapped = mapPurchasedShoppingList(dto)
expect(mapped.list.created_date).toBeInstanceOf(Date)
expect(mapped.list.items[0].recipe.name).toBe('Omelette')
})
})

27
tests/units.test.js Normal file
View file

@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest'
import { getConversionFactor, calculateTotals } from '@/units'
describe('units', () => {
it('returns conversion factors for aliases and base units', () => {
expect(getConversionFactor('kg')).toEqual({ unit: 'kg', factor: 1 })
expect(getConversionFactor('g')).toEqual({ unit: 'kg', factor: 1000 })
expect(getConversionFactor('kgs')).toEqual({ unit: 'kg', factor: 1 })
expect(getConversionFactor('litre')).toEqual({ unit: 'litres', factor: 1 })
expect(getConversionFactor('ml')).toEqual({ unit: 'litres', factor: 1000 })
})
it('calculates totals grouped by base units', () => {
const totals = calculateTotals([
{ quantity: 500, unit: 'g' },
{ quantity: 0.5, unit: 'kg' },
{ quantity: 250, unit: 'ml' },
{ quantity: 0.75, unit: 'litre' },
])
// Expect kg total = 0.5 (from g) + 0.5 (from kg) = 1
const kgTotal = totals.find((t) => t.unit === 'kg')
expect(kgTotal.quantity).toBeCloseTo(1)
// Expect litres total = 0.25 (from ml) + 0.75 (from litre) = 1
const lTotal = totals.find((t) => t.unit === 'litres')
expect(lTotal.quantity).toBeCloseTo(1)
})
})

16
vitest.config.js Normal file
View file

@ -0,0 +1,16 @@
import { defineConfig } from 'vitest/config'
import { fileURLToPath } from 'node:url'
export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
test: {
environment: 'node',
include: ['tests/**/*.{test,spec}.js'],
globals: true,
reporters: 'default',
},
})