import { test, expect } from '@playwright/test' /** * E2E tests for Product Category CRUD operations. * * Prerequisites: * - Frontend running on http://localhost:3001 (npm run dev) * - Backend running on http://localhost:8081 (docker compose up) * - Auth setup must run first (profile selected) */ const UNIQUE = Date.now() const CATEGORY_NAME = `E2E Catégorie Produit ${UNIQUE}` const CATEGORY_NOTES = `Notes de test automatisé ${UNIQUE}` const CATEGORY_NAME_UPDATED = `${CATEGORY_NAME} modifié` const CATEGORY_NOTES_UPDATED = `${CATEGORY_NOTES} — mis à jour` test.describe('Product Category CRUD', () => { test.describe.configure({ mode: 'serial' }) // ────────────────────────────────────────────── // CREATE // ────────────────────────────────────────────── test('should display the product category list page', async ({ page }) => { await page.goto('/product-category') await expect(page.getByRole('heading', { name: 'Catégories de produit' })).toBeVisible({ timeout: 10_000 }) await expect(page.getByText('Catégories enregistrées')).toBeVisible() }) test('should navigate to the create form', async ({ page }) => { await page.goto('/product-category') // The toolbar button text is "Créer" (with a plus icon) await page.getByRole('button', { name: /créer/i }).click() await expect(page).toHaveURL('/product-category/new') await expect(page.getByRole('heading', { name: 'Nouvelle catégorie de produit' })).toBeVisible() }) test('should show validation error for short name', async ({ page }) => { await page.goto('/product-category/new') await page.locator('#model-type-name').fill('A') // The form submit button in ModelTypeForm is also "Créer" await page.locator('button[type="submit"]').click() await expect(page.getByText('Le nom doit contenir au moins 2 caractères')).toBeVisible() }) test('should create a new product category', async ({ page }) => { await page.goto('/product-category/new') await page.locator('#model-type-name').fill(CATEGORY_NAME) await page.locator('#model-type-notes').fill(CATEGORY_NOTES) // Verify category is locked to PRODUCT const categorySelect = page.locator('#model-type-category') await expect(categorySelect).toBeDisabled() await expect(categorySelect).toHaveValue('PRODUCT') await page.locator('button[type="submit"]').click() // Should redirect to list and show success toast await expect(page).toHaveURL('/product-category', { timeout: 10_000 }) await expect(page.getByText('Catégorie de produit créée avec succès')).toBeVisible() }) // ────────────────────────────────────────────── // READ // ────────────────────────────────────────────── test('should display the created category in the list', async ({ page }) => { await page.goto('/product-category') // Target the table cell specifically (desktop view also renders a mobile card) await expect(page.getByRole('cell', { name: CATEGORY_NAME })).toBeVisible({ timeout: 10_000 }) }) test('should find the category via search', async ({ page }) => { await page.goto('/product-category') // Type in search input (placeholder: "Rechercher par nom…") const searchInput = page.getByPlaceholder('Rechercher par nom…') await searchInput.fill(UNIQUE.toString()) // Wait for debounce (300ms) + API response await page.waitForTimeout(500) await expect(page.getByRole('cell', { name: CATEGORY_NAME })).toBeVisible({ timeout: 5_000 }) }) // ────────────────────────────────────────────── // UPDATE // ────────────────────────────────────────────── test('should navigate to the edit page', async ({ page }) => { await page.goto('/product-category') await expect(page.getByRole('cell', { name: CATEGORY_NAME })).toBeVisible({ timeout: 10_000 }) // Find the row with our category and click "Éditer" const row = page.getByRole('row').filter({ hasText: CATEGORY_NAME }) await row.getByRole('button', { name: 'Éditer' }).click() await expect(page.getByRole('heading', { name: /modifier/i })).toBeVisible({ timeout: 10_000 }) }) test('should edit the category name and notes', async ({ page }) => { await page.goto('/product-category') await expect(page.getByRole('cell', { name: CATEGORY_NAME })).toBeVisible({ timeout: 10_000 }) const row = page.getByRole('row').filter({ hasText: CATEGORY_NAME }) await row.getByRole('button', { name: 'Éditer' }).click() await expect(page.getByRole('heading', { name: /modifier/i })).toBeVisible({ timeout: 10_000 }) // Update name const nameInput = page.locator('#model-type-name') await nameInput.clear() await nameInput.fill(CATEGORY_NAME_UPDATED) // Update notes const notesTextarea = page.locator('#model-type-notes') await notesTextarea.clear() await notesTextarea.fill(CATEGORY_NOTES_UPDATED) await page.locator('button[type="submit"]').click() // Should redirect and show success await expect(page).toHaveURL('/product-category', { timeout: 10_000 }) await expect(page.getByText('Catégorie de produit mise à jour avec succès')).toBeVisible() }) test('should display updated category in the list', async ({ page }) => { await page.goto('/product-category') await expect(page.getByRole('cell', { name: CATEGORY_NAME_UPDATED })).toBeVisible({ timeout: 10_000 }) }) // ────────────────────────────────────────────── // DELETE // ────────────────────────────────────────────── test('should cancel deletion when clicking Annuler', async ({ page }) => { await page.goto('/product-category') await expect(page.getByRole('cell', { name: CATEGORY_NAME_UPDATED })).toBeVisible({ timeout: 10_000 }) const row = page.getByRole('row').filter({ hasText: CATEGORY_NAME_UPDATED }) await row.getByRole('button', { name: 'Supprimer' }).click() // Confirmation modal should appear await expect(page.getByText('Supprimer ce type ?')).toBeVisible() await page.getByRole('button', { name: 'Annuler' }).click() // Category should still be present await expect(page.getByRole('cell', { name: CATEGORY_NAME_UPDATED })).toBeVisible() }) test('should delete the category', async ({ page }) => { await page.goto('/product-category') await expect(page.getByRole('cell', { name: CATEGORY_NAME_UPDATED })).toBeVisible({ timeout: 10_000 }) const row = page.getByRole('row').filter({ hasText: CATEGORY_NAME_UPDATED }) await row.getByRole('button', { name: 'Supprimer' }).click() // Confirm deletion in modal await expect(page.getByText('Supprimer ce type ?')).toBeVisible() // Click the confirm "Supprimer" button inside the modal (btn-error style) await page.locator('button.btn-error').filter({ hasText: 'Supprimer' }).click() // Should show success toast and category should disappear await expect(page.getByText(/supprimé avec succès/i)).toBeVisible({ timeout: 10_000 }) await expect(page.getByRole('cell', { name: CATEGORY_NAME_UPDATED })).not.toBeVisible() }) })