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:
73
tests/e2e/converter.spec.js
Normal file
73
tests/e2e/converter.spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
78
tests/e2e/meal-planner.spec.js
Normal file
78
tests/e2e/meal-planner.spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
42
tests/e2e/navigation.spec.js
Normal file
42
tests/e2e/navigation.spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
89
tests/e2e/shopping-list.spec.js
Normal file
89
tests/e2e/shopping-list.spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user