test: aggiungi suite completa unit, integration ed e2e

- Unit (12+9): conversion.js (rawToCooked/cookedToRaw, edge case, inversa)
  e storage.js (save/load, round-trip, default fallback)
- Integration (17+12+14): Converter (ricerca, selezione, calcolo, swap, reset),
  MealPlanner (rendering, add/remove, generateShopping, deduplicazione),
  ShoppingList (add, toggle, remove, clearAll, contatore)
- E2E Playwright (6+6+7+10): navigation, meal-planner, converter, shopping-list
- Configurazione: vitest.config.js + playwright.config.js + tests/setup.js
- Script: test, test:coverage, test:e2e, test:e2e:ui

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 14:45:31 +01:00
parent 09c9706938
commit e10bb2df4c
14 changed files with 3092 additions and 5 deletions

View File

@@ -0,0 +1,73 @@
import { test, expect } from '@playwright/test'
test.describe('Convertitore', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.locator('.nav-btn', { hasText: 'Converti' }).click()
})
test('mostra il messaggio iniziale prima di cercare', async ({ page }) => {
await expect(page.locator('.hint-state')).toBeVisible()
await expect(page.locator('.converter-card')).not.toBeVisible()
})
test('cerca un alimento e mostra i risultati', async ({ page }) => {
await page.locator('input[type="text"]').fill('riso')
await expect(page.locator('.result-item').first()).toBeVisible()
})
test('i nomi nella lista hanno solo l\'iniziale maiuscola', async ({ page }) => {
await page.locator('input[type="text"]').fill('pollo')
const firstFood = await page.locator('.result-food').first().textContent()
// "Pollo petto" → solo la prima lettera maiuscola
expect(firstFood[0]).toBe(firstFood[0].toUpperCase())
if (firstFood.includes(' ')) {
const secondWord = firstFood.split(' ')[1]
expect(secondWord[0]).toBe(secondWord[0].toLowerCase())
}
})
test('seleziona un alimento e mostra la converter card', async ({ page }) => {
await page.locator('input[type="text"]').fill('riso basmati')
await page.locator('.result-item').first().click()
await expect(page.locator('.converter-card')).toBeVisible()
await expect(page.locator('.result-item')).toHaveCount(0)
})
test('calcola il peso cotto inserendo i grammi', async ({ page }) => {
await page.locator('input[type="text"]').fill('riso basmati')
await page.locator('.result-item').first().click()
await page.locator('.calc-input').fill('100')
// Riso basmati ha fattore 3.0 → 300g cotto
await expect(page.locator('.output-value')).toBeVisible()
const result = await page.locator('.output-value').textContent()
expect(parseFloat(result)).toBeCloseTo(300, 0)
})
test('il pulsante ⇄ inverte la direzione crudo↔cotto', async ({ page }) => {
await page.locator('input[type="text"]').fill('pasta')
await page.locator('.result-item').first().click()
const labelBefore = await page.locator('.calc-label').first().textContent()
await page.locator('.btn-swap').click()
const labelAfter = await page.locator('.calc-label').first().textContent()
expect(labelBefore).not.toBe(labelAfter)
})
test('il pulsante Cambia torna alla ricerca', async ({ page }) => {
await page.locator('input[type="text"]').fill('riso')
await page.locator('.result-item').first().click()
await page.locator('.btn-reset').click()
await expect(page.locator('.converter-card')).not.toBeVisible()
await expect(page.locator('input[type="text"]')).not.toBeDisabled()
})
test('mostra il footer con fattore di resa quando c\'è un risultato', async ({ page }) => {
await page.locator('input[type="text"]').fill('riso')
await page.locator('.result-item').first().click()
await page.locator('.calc-input').fill('100')
await expect(page.locator('.card-footer')).toContainText('fattore di resa')
})
})

View File

@@ -0,0 +1,78 @@
import { test, expect } from '@playwright/test'
test.describe('Piano Pasti', () => {
test.beforeEach(async ({ page }) => {
// Pulisce localStorage per partire da uno stato noto
await page.goto('/')
await page.evaluate(() => localStorage.clear())
await page.reload()
})
test('mostra 7 card giornaliere', async ({ page }) => {
const cards = page.locator('.meal-card')
await expect(cards).toHaveCount(7)
})
test('il giorno corrente è espanso di default', async ({ page }) => {
// Almeno una card deve essere aperta (class "open")
await expect(page.locator('.meal-card.open')).toHaveCount(1)
})
test('si può espandere e chiudere una card con tap', async ({ page }) => {
const firstHeader = page.locator('.card-header').first()
const firstCard = page.locator('.meal-card').first()
// Se la prima card è già aperta, chiudila prima
const isOpen = await firstCard.evaluate(el => el.classList.contains('open'))
await firstHeader.click()
if (isOpen) {
await expect(firstCard).not.toHaveClass(/open/)
} else {
await expect(firstCard).toHaveClass(/open/)
}
})
test('aggiunge un alimento al pranzo del giorno corrente', async ({ page }) => {
const openCard = page.locator('.meal-card.open')
const pranzoInput = openCard.locator('.meal-slot').nth(1).locator('input[type="text"]')
await pranzoInput.fill('pasta al pomodoro')
await pranzoInput.press('Enter')
await expect(openCard.locator('.item-text', { hasText: 'pasta al pomodoro' })).toBeVisible()
})
test('rimuove un alimento con il pulsante ×', async ({ page }) => {
const openCard = page.locator('.meal-card.open')
const pranzoInput = openCard.locator('.meal-slot').nth(1).locator('input[type="text"]')
await pranzoInput.fill('riso')
await pranzoInput.press('Enter')
const itemRow = openCard.locator('.item-row', { hasText: 'riso' })
await expect(itemRow).toBeVisible()
await itemRow.locator('.btn-remove').click()
await expect(itemRow).not.toBeVisible()
})
test('genera la lista della spesa e passa alla tab Spesa', async ({ page }) => {
const openCard = page.locator('.meal-card.open')
const cenahInput = openCard.locator('.meal-slot').nth(2).locator('input[type="text"]')
await cenahInput.fill('pollo')
await cenahInput.press('Enter')
await page.locator('.btn-generate').click()
// Deve essere passato alla tab Spesa
await expect(page.locator('.page-title')).toContainText('Lista della spesa')
await expect(page.locator('.item-name', { hasText: 'pollo' })).toBeVisible()
})
test('i dati persistono dopo il reload', async ({ page }) => {
const openCard = page.locator('.meal-card.open')
const colazioneInput = openCard.locator('.meal-slot').first().locator('input[type="text"]')
await colazioneInput.fill('caffè')
await colazioneInput.press('Enter')
await page.reload()
await expect(page.locator('.meal-card.open .item-text', { hasText: 'caffè' })).toBeVisible()
})
})

View File

@@ -0,0 +1,42 @@
import { test, expect } from '@playwright/test'
test.describe('Navigazione tra tab', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('la tab Pasti è attiva al caricamento', async ({ page }) => {
await expect(page.locator('.nav-btn.active')).toContainText('Pasti')
await expect(page.locator('.page-title')).toContainText('Piano Pasti')
})
test('la tab Converti mostra il convertitore', async ({ page }) => {
await page.locator('.nav-btn', { hasText: 'Converti' }).click()
await expect(page.locator('.page-title')).toContainText('Convertitore')
await expect(page.locator('.nav-btn.active')).toContainText('Converti')
})
test('la tab Spesa mostra la lista della spesa', async ({ page }) => {
await page.locator('.nav-btn', { hasText: 'Spesa' }).click()
await expect(page.locator('.page-title')).toContainText('Lista della spesa')
await expect(page.locator('.nav-btn.active')).toContainText('Spesa')
})
test('si può tornare a Pasti da un\'altra tab', async ({ page }) => {
await page.locator('.nav-btn', { hasText: 'Converti' }).click()
await page.locator('.nav-btn', { hasText: 'Pasti' }).click()
await expect(page.locator('.page-title')).toContainText('Piano Pasti')
})
test('il pulsante info apre il pannello informazioni', async ({ page }) => {
await page.locator('.btn-info').click()
await expect(page.locator('.sheet')).toBeVisible()
await expect(page.locator('.app-name')).toContainText('BitePlan')
})
test('il pannello info si chiude con la X', async ({ page }) => {
await page.locator('.btn-info').click()
await page.locator('.btn-x').click()
await expect(page.locator('.sheet')).not.toBeVisible()
})
})

View File

@@ -0,0 +1,89 @@
import { test, expect } from '@playwright/test'
test.describe('Lista della spesa', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.evaluate(() => localStorage.clear())
await page.locator('.nav-btn', { hasText: 'Spesa' }).click()
})
test('mostra stato vuoto con lista vuota', async ({ page }) => {
await expect(page.locator('.empty-state')).toBeVisible()
})
test('aggiunge un elemento tramite il pulsante +', async ({ page }) => {
await page.locator('input[type="text"]').fill('latte')
await page.locator('.btn-add').click()
await expect(page.locator('.item-name', { hasText: 'latte' })).toBeVisible()
})
test('aggiunge un elemento con il tasto Invio', async ({ page }) => {
await page.locator('input[type="text"]').fill('burro')
await page.locator('input[type="text"]').press('Enter')
await expect(page.locator('.item-name', { hasText: 'burro' })).toBeVisible()
})
test('svuota il campo dopo l\'aggiunta', async ({ page }) => {
await page.locator('input[type="text"]').fill('olio')
await page.locator('.btn-add').click()
await expect(page.locator('input[type="text"]')).toHaveValue('')
})
test('spunta un elemento e lo sposta nei completati', async ({ page }) => {
await page.locator('input[type="text"]').fill('pasta')
await page.locator('.btn-add').click()
await page.locator('.checkbox-item').first().locator('input[type="checkbox"]').click()
await expect(page.locator('.section-divider')).toBeVisible()
await expect(page.locator('.muted .item-name', { hasText: 'pasta' })).toBeVisible()
})
test('rimuove un singolo elemento con ×', async ({ page }) => {
await page.locator('input[type="text"]').fill('farina')
await page.locator('.btn-add').click()
await page.locator('.checkbox-item').first().locator('.btn-remove').click()
await expect(page.locator('.item-name', { hasText: 'farina' })).not.toBeVisible()
await expect(page.locator('.empty-state')).toBeVisible()
})
test('svuota lista con conferma del dialog', async ({ page }) => {
await page.locator('input[type="text"]').fill('test')
await page.locator('.btn-add').click()
page.once('dialog', dialog => dialog.accept())
await page.locator('.btn-clear').click()
await expect(page.locator('.empty-state')).toBeVisible()
})
test('non svuota lista se si annulla il dialog', async ({ page }) => {
await page.locator('input[type="text"]').fill('test')
await page.locator('.btn-add').click()
page.once('dialog', dialog => dialog.dismiss())
await page.locator('.btn-clear').click()
await expect(page.locator('.item-name', { hasText: 'test' })).toBeVisible()
})
test('il contatore mostra elementi completati / totale', async ({ page }) => {
await page.locator('input[type="text"]').fill('a')
await page.locator('.btn-add').click()
await page.locator('input[type="text"]').fill('b')
await page.locator('.btn-add').click()
await page.locator('.checkbox-item').first().locator('input[type="checkbox"]').click()
const subtitle = await page.locator('.page-subtitle').textContent()
expect(subtitle).toMatch(/1/)
expect(subtitle).toMatch(/2/)
})
test('i dati persistono dopo il reload', async ({ page }) => {
await page.locator('input[type="text"]').fill('yogurt')
await page.locator('.btn-add').click()
await page.reload()
await page.locator('.nav-btn', { hasText: 'Spesa' }).click()
await expect(page.locator('.item-name', { hasText: 'yogurt' })).toBeVisible()
})
})