From d88e202bb32a43c3bd456609ebd1eb11f03fce21 Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Mon, 6 Apr 2026 20:35:01 +0000 Subject: [PATCH] =?UTF-8?q?Fix=20critical=20bugs:=20oil=20prices=20=C2=A50?= =?UTF-8?q?.00,=20ingredient=20field=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - oils store: change Map to plain object for Vue reactivity - recipes store: map `oil_name` from API (was only mapping `oil`/`name`) - OilReference: fix .get() calls to bracket access - Add price-display.cy.js regression test (3 tests) - Add visual-check.cy.js for screenshot verification Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/cypress/e2e/price-display.cy.js | 39 +++++++++++++++++ frontend/cypress/e2e/visual-check.cy.js | 55 ++++++++++++++++++++++++ frontend/src/stores/oils.js | 24 +++++------ frontend/src/stores/recipes.js | 2 +- frontend/src/views/OilReference.vue | 4 +- 5 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 frontend/cypress/e2e/price-display.cy.js create mode 100644 frontend/cypress/e2e/visual-check.cy.js diff --git a/frontend/cypress/e2e/price-display.cy.js b/frontend/cypress/e2e/price-display.cy.js new file mode 100644 index 0000000..6c2a21a --- /dev/null +++ b/frontend/cypress/e2e/price-display.cy.js @@ -0,0 +1,39 @@ +describe('Price Display Regression', () => { + it('recipe cards show non-zero prices', () => { + cy.visit('/') + cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1) + cy.wait(2000) // wait for oils store to load and re-render + + // Check via .card-price elements which hold the formatted cost + cy.get('.card-price').first().invoke('text').then(text => { + const match = text.match(/¥\s*(\d+\.?\d*)/) + expect(match, 'Card price should contain ¥').to.not.be.null + expect(parseFloat(match[1]), 'Price should be > 0').to.be.gt(0) + }) + }) + + it('oil reference page shows non-zero prices', () => { + cy.visit('/oils') + cy.get('.oil-card', { timeout: 10000 }).should('have.length.gte', 1) + cy.wait(500) + + cy.get('.oil-card').first().invoke('text').then(text => { + const match = text.match(/¥\s*(\d+\.?\d*)/) + expect(match, 'Oil card should contain a price').to.not.be.null + expect(parseFloat(match[1])).to.be.gt(0) + }) + }) + + it('recipe detail shows non-zero total cost', () => { + cy.visit('/') + cy.get('.recipe-card', { timeout: 10000 }).first().click() + cy.wait(1000) + + // Look for any ¥ amount > 0 in the detail overlay + cy.get('[class*="overlay"], [class*="detail"]').invoke('text').then(text => { + const prices = [...text.matchAll(/¥\s*(\d+\.?\d*)/g)].map(m => parseFloat(m[1])) + const nonZero = prices.filter(p => p > 0) + expect(nonZero.length, 'Detail should show at least one non-zero price').to.be.gte(1) + }) + }) +}) diff --git a/frontend/cypress/e2e/visual-check.cy.js b/frontend/cypress/e2e/visual-check.cy.js new file mode 100644 index 0000000..68549bc --- /dev/null +++ b/frontend/cypress/e2e/visual-check.cy.js @@ -0,0 +1,55 @@ +// Quick visual screenshots for manual review before deploy +const ADMIN_TOKEN = 'c86ae7afbe10fabe3c1d5e1a7fee74feaadfd5dc7be2ab62' + +describe('Visual Check - Screenshots', () => { + it('homepage with recipes', () => { + cy.visit('/', { onBeforeLoad(win) { win.localStorage.setItem('oil_auth_token', ADMIN_TOKEN) } }) + cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1) + cy.wait(1000) + cy.screenshot('01-homepage') + }) + + it('recipe detail overlay', () => { + cy.visit('/', { onBeforeLoad(win) { win.localStorage.setItem('oil_auth_token', ADMIN_TOKEN) } }) + cy.get('.recipe-card', { timeout: 10000 }).first().click() + cy.wait(1000) + cy.screenshot('02-recipe-detail') + }) + + it('oil reference page', () => { + cy.visit('/oils', { onBeforeLoad(win) { win.localStorage.setItem('oil_auth_token', ADMIN_TOKEN) } }) + cy.get('.oil-card', { timeout: 10000 }).should('have.length.gte', 1) + cy.wait(500) + cy.screenshot('03-oil-reference') + }) + + it('manage recipes page', () => { + cy.visit('/manage', { onBeforeLoad(win) { win.localStorage.setItem('oil_auth_token', ADMIN_TOKEN) } }) + cy.wait(2000) + cy.screenshot('04-manage-recipes') + }) + + it('inventory page', () => { + cy.visit('/inventory', { onBeforeLoad(win) { win.localStorage.setItem('oil_auth_token', ADMIN_TOKEN) } }) + cy.wait(1500) + cy.screenshot('05-inventory') + }) + + it('check if recipe cards show price > 0', () => { + cy.visit('/', { onBeforeLoad(win) { win.localStorage.setItem('oil_auth_token', ADMIN_TOKEN) } }) + cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1) + // Check if any card shows a non-zero price + cy.get('.recipe-card').first().invoke('text').then(text => { + cy.log('First card text: ' + text) + // Check if it contains a price like ¥ X.XX where X > 0 + const priceMatch = text.match(/¥\s*(\d+\.?\d*)/) + if (priceMatch) { + cy.log('Price found: ¥' + priceMatch[1]) + const price = parseFloat(priceMatch[1]) + expect(price, 'Recipe card should show price > 0').to.be.gt(0) + } else { + cy.log('WARNING: No price found on recipe card') + } + }) + }) +}) diff --git a/frontend/src/stores/oils.js b/frontend/src/stores/oils.js index d5df3bc..2b2f647 100644 --- a/frontend/src/stores/oils.js +++ b/frontend/src/stores/oils.js @@ -13,16 +13,16 @@ export const VOLUME_DROPS = { } export const useOilsStore = defineStore('oils', () => { - const oils = ref(new Map()) - const oilsMeta = ref(new Map()) + const oils = ref({}) + const oilsMeta = ref({}) // Getters const oilNames = computed(() => - [...oils.value.keys()].sort((a, b) => a.localeCompare(b, 'zh')) + Object.keys(oils.value).sort((a, b) => a.localeCompare(b, 'zh')) ) function pricePerDrop(name) { - return oils.value.get(name) || 0 + return oils.value[name] || 0 } function calcCost(ingredients) { @@ -33,7 +33,7 @@ export const useOilsStore = defineStore('oils', () => { function calcRetailCost(ingredients) { return ingredients.reduce((sum, ing) => { - const meta = oilsMeta.value.get(ing.oil) + const meta = oilsMeta.value[ing.oil] if (meta && meta.retailPrice && meta.dropCount) { return sum + (meta.retailPrice / meta.dropCount) * ing.drops } @@ -58,17 +58,17 @@ export const useOilsStore = defineStore('oils', () => { // Actions async function loadOils() { const data = await api.get('/api/oils') - const newOils = new Map() - const newMeta = new Map() + const newOils = {} + const newMeta = {} for (const oil of data) { const ppd = oil.drop_count ? oil.bottle_price / oil.drop_count : 0 - newOils.set(oil.name, ppd) - newMeta.set(oil.name, { + newOils[oil.name] = ppd + newMeta[oil.name] = { bottlePrice: oil.bottle_price, dropCount: oil.drop_count, retailPrice: oil.retail_price ?? null, isActive: oil.is_active ?? true, - }) + } } oils.value = newOils oilsMeta.value = newMeta @@ -86,8 +86,8 @@ export const useOilsStore = defineStore('oils', () => { async function deleteOil(name) { await api.delete(`/api/oils/${encodeURIComponent(name)}`) - oils.value.delete(name) - oilsMeta.value.delete(name) + delete oils.value[name] + delete oilsMeta.value[name] } return { diff --git a/frontend/src/stores/recipes.js b/frontend/src/stores/recipes.js index 8414741..0f494aa 100644 --- a/frontend/src/stores/recipes.js +++ b/frontend/src/stores/recipes.js @@ -19,7 +19,7 @@ export const useRecipesStore = defineStore('recipes', () => { note: r.note ?? '', tags: r.tags ?? [], ingredients: (r.ingredients ?? []).map((ing) => ({ - oil: ing.oil ?? ing.name, + oil: ing.oil_name ?? ing.oil ?? ing.name, drops: ing.drops, })), })) diff --git a/frontend/src/views/OilReference.vue b/frontend/src/views/OilReference.vue index 97bfc4b..b979c96 100644 --- a/frontend/src/views/OilReference.vue +++ b/frontend/src/views/OilReference.vue @@ -247,7 +247,7 @@ const recipesWithOil = computed(() => { }) function getMeta(name) { - return oils.oilsMeta.get(name) + return oils.oilsMeta[name] } function getDropsForOil(recipe, oilName) { @@ -280,7 +280,7 @@ async function addOil() { function editOil(name) { editingOilName.value = name - const meta = oils.oilsMeta.get(name) + const meta = oils.oilsMeta[name] editBottlePrice.value = meta?.bottlePrice || 0 editDropCount.value = meta?.dropCount || 0 editRetailPrice.value = meta?.retailPrice || null