Files
oil-formula-calculator/frontend/cypress/e2e/oil-data-integrity.cy.js
Hera Zhao ee8ec23dc7 Refactor frontend to Vue 3 + Vite + Pinia + Cypress E2E
- Replace single-file 8441-line HTML with Vue 3 SPA
- Pinia stores: auth, oils, recipes, diary, ui
- Composables: useApi, useDialog, useSmartPaste, useOilTranslation
- 6 shared components: RecipeCard, RecipeDetailOverlay, TagPicker, etc.
- 9 page views: RecipeSearch, RecipeManager, Inventory, OilReference, etc.
- 14 Cypress E2E test specs (113 tests), all passing
- Multi-stage Dockerfile (Node build + Python runtime)
- Demo video generation scripts (TTS + subtitles + screen recording)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:35:00 +00:00

108 lines
3.7 KiB
JavaScript

describe('Oil Data Integrity', () => {
it('all oils have valid prices', () => {
cy.request('/api/oils').then(res => {
res.body.forEach(oil => {
expect(oil.name).to.be.a('string').and.not.be.empty
expect(oil.bottle_price).to.be.a('number').and.be.gte(0)
expect(oil.drop_count).to.be.a('number').and.be.gt(0)
})
})
})
it('common oils exist in the database', () => {
const expected = ['薰衣草', '茶树', '乳香', '柠檬', '椒样薄荷', '椰子油']
cy.request('/api/oils').then(res => {
const names = res.body.map(o => o.name)
expected.forEach(name => {
expect(names).to.include(name)
})
})
})
it('oil price per drop is correctly calculated', () => {
cy.request('/api/oils').then(res => {
res.body.forEach(oil => {
const ppd = oil.bottle_price / oil.drop_count
expect(ppd).to.be.a('number')
expect(ppd).to.be.gte(0)
expect(ppd).to.be.lte(100) // sanity check: no oil costs >100 per drop
})
})
})
it('drop counts match known volume standards', () => {
// Standard doTERRA volumes: 5ml=93, 10ml=186, 15ml=280
const validDropCounts = [46, 93, 160, 186, 280, 2146]
cy.request('/api/oils').then(res => {
const counts = new Set(res.body.map(o => o.drop_count))
// At least some should match standard volumes
const matching = [...counts].filter(c => validDropCounts.includes(c))
expect(matching.length).to.be.gte(1)
})
})
})
describe('Recipe Data Integrity', () => {
it('all recipes have valid structure', () => {
cy.request('/api/recipes').then(res => {
expect(res.body.length).to.be.gte(1)
res.body.forEach(recipe => {
expect(recipe).to.have.property('id')
expect(recipe).to.have.property('name').and.not.be.empty
expect(recipe).to.have.property('ingredients').and.be.an('array')
recipe.ingredients.forEach(ing => {
expect(ing).to.have.property('oil_name').and.not.be.empty
expect(ing).to.have.property('drops').and.be.a('number').and.be.gt(0)
})
})
})
})
it('recipes reference existing oils', () => {
cy.request('/api/oils').then(oilRes => {
const oilNames = new Set(oilRes.body.map(o => o.name))
cy.request('/api/recipes').then(recipeRes => {
let totalMissing = 0
recipeRes.body.forEach(recipe => {
recipe.ingredients.forEach(ing => {
if (!oilNames.has(ing.oil_name)) totalMissing++
})
})
// Allow some missing (discontinued oils), but not too many
const totalIngs = recipeRes.body.reduce((s, r) => s + r.ingredients.length, 0)
const missingRate = totalMissing / totalIngs
expect(missingRate).to.be.lt(0.2) // less than 20% missing
})
})
})
it('no duplicate recipe names', () => {
cy.request('/api/recipes').then(res => {
const names = res.body.map(r => r.name)
const unique = new Set(names)
// Allow some duplicates but flag if many
const dupRate = (names.length - unique.size) / names.length
expect(dupRate).to.be.lt(0.1) // less than 10% duplicates
})
})
it('recipe costs are calculable', () => {
cy.request('/api/oils').then(oilRes => {
const oilPrices = {}
oilRes.body.forEach(o => {
oilPrices[o.name] = o.bottle_price / o.drop_count
})
cy.request('/api/recipes').then(recipeRes => {
recipeRes.body.slice(0, 20).forEach(recipe => {
let cost = 0
recipe.ingredients.forEach(ing => {
cost += (oilPrices[ing.oil_name] || 0) * ing.drops
})
expect(cost).to.be.a('number')
expect(cost).to.be.gte(0)
})
})
})
})
})