Add comprehensive test suite: 105 unit + 167 E2E tests

- Vitest unit tests: smart paste parsing (37), cost calculations (21),
  oil translation (16), dialog system (12), with production data fixtures
- Cypress E2E tests: API CRUD (27), auth flow (8), recipe detail (10),
  search (12), oil reference (4), favorites (6), inventory (6),
  recipe management (10), diary (11), bug tracker (8), user management (13),
  cost parity (6), data integrity (8), responsive (9), performance (6),
  navigation (8), admin flow (5)
- Test coverage doc with prioritized gap analysis
- Found backend bug: POST /api/bug-reports/{id}/comment deletes the bug

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 19:47:47 +00:00
parent ee8ec23dc7
commit 2491479c2c
16 changed files with 3232 additions and 3 deletions

View File

@@ -0,0 +1,88 @@
describe('Recipe Cost Parity Test', () => {
// Verify recipe cost formula: cost = sum(bottle_price / drop_count * drops)
let oilsMap = {}
let testRecipes = []
before(() => {
cy.request('/api/oils').then(res => {
res.body.forEach(oil => {
oilsMap[oil.name] = {
bottle_price: oil.bottle_price,
drop_count: oil.drop_count,
ppd: oil.drop_count ? oil.bottle_price / oil.drop_count : 0,
retail_price: oil.retail_price
}
})
})
cy.request('/api/recipes').then(res => {
testRecipes = res.body.slice(0, 20)
})
})
it('oil data has correct structure (137+ oils)', () => {
expect(Object.keys(oilsMap).length).to.be.gte(100)
const lav = oilsMap['薰衣草']
expect(lav).to.exist
expect(lav.bottle_price).to.be.gt(0)
expect(lav.drop_count).to.be.gt(0)
})
it('price-per-drop matches formula for common oils', () => {
const checks = ['薰衣草', '乳香', '茶树', '柠檬', '椒样薄荷']
checks.forEach(name => {
const oil = oilsMap[name]
if (oil) {
const expected = oil.bottle_price / oil.drop_count
expect(oil.ppd).to.be.closeTo(expected, 0.0001)
}
})
})
it('calculates cost for each of first 20 recipes', () => {
testRecipes.forEach(recipe => {
let cost = 0
recipe.ingredients.forEach(ing => {
const oil = oilsMap[ing.oil_name]
if (oil) cost += oil.ppd * ing.drops
})
expect(cost).to.be.gte(0)
})
})
it('retail price >= wholesale for oils that have it', () => {
Object.entries(oilsMap).forEach(([name, oil]) => {
if (oil.retail_price && oil.retail_price > 0) {
expect(oil.retail_price).to.be.gte(oil.bottle_price)
}
})
})
it('no recipe has all-zero cost', () => {
let zeroCostCount = 0
testRecipes.forEach(recipe => {
let cost = 0
recipe.ingredients.forEach(ing => {
const oil = oilsMap[ing.oil_name]
if (oil) cost += oil.ppd * ing.drops
})
if (cost === 0) zeroCostCount++
})
expect(zeroCostCount).to.be.lt(testRecipes.length)
})
it('cost formula is consistent: two calculation methods agree', () => {
testRecipes.forEach(recipe => {
const costs = recipe.ingredients.map(ing => {
const oil = oilsMap[ing.oil_name]
return oil ? oil.ppd * ing.drops : 0
})
const fromMap = costs.reduce((a, b) => a + b, 0)
const fromReduce = recipe.ingredients.reduce((s, ing) => {
const oil = oilsMap[ing.oil_name]
return s + (oil ? oil.ppd * ing.drops : 0)
}, 0)
expect(fromMap).to.be.closeTo(fromReduce, 0.001)
})
})
})