import { test, expect } from '@playwright/test'; import * as auth from '../fixtures/auth'; import * as assertions from '../utils/assertions'; import * as helpers from '../utils/helpers'; import { LOCAL_USERS } from '../fixtures/test-data'; test.describe('AI Extraction + Auto-Photo-Save Flow', () => { const BASE_URL = process.env.BASE_URL || 'http://localhost:8917'; test.beforeEach(async ({ page }) => { // Navigate to app and login await page.goto(BASE_URL); await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); await assertions.assertUserAuthenticated(page, BASE_URL); // Navigate to new item creation (AI extraction) await page.goto(`${BASE_URL}/inventory/new`); // Wait for AI wizard to be ready const aiWizard = page.locator('[data-testid="ai-onboarding-wizard"]'); await expect(aiWizard).toBeVisible({ timeout: 5000 }); }); test('should auto-save photo after successful AI identification and item creation', async ({ page, }) => { // 1. Click capture button to trigger AI extraction const captureButton = page.locator('[data-testid="capture-button"]'); await expect(captureButton).toBeVisible(); await captureButton.click(); // 2. Wait for AI extraction to complete and show results const resultsForm = page.locator('[data-testid="extraction-results-form"]'); await expect(resultsForm).toBeVisible({ timeout: 15000 }); // 3. Verify extracted data is shown const nameField = page.locator('[data-testid="extracted-name"]'); await expect(nameField).toBeVisible(); const extractedName = await nameField.inputValue(); expect(extractedName).toBeTruthy(); expect(extractedName.length).toBeGreaterThan(0); // 4. Confirm extraction to create item const confirmButton = page.locator('[data-testid="confirm-extraction"]'); await confirmButton.click(); // 5. Wait for success toast and item creation const successToast = page.locator('[data-testid="toast-success"]'); await expect(successToast).toBeVisible({ timeout: 10000 }); // Verify toast contains success message about item creation const toastText = await successToast.textContent(); expect(toastText?.toLowerCase()).toContain('created'); // 6. Wait for redirect to inventory await page.waitForURL(/inventory|items/, { timeout: 10000 }); // 7. Navigate to inventory to verify photo appears await page.goto(`${BASE_URL}/inventory`); const inventoryTable = page.locator('[data-testid="inventory-table"]'); await expect(inventoryTable).toBeVisible({ timeout: 5000 }); // 8. Get the first item row (should be the newly created item) const firstItemRow = inventoryTable.locator('tbody tr').first(); await expect(firstItemRow).toBeVisible(); // 9. Verify photo thumbnail is present const photoThumbnail = firstItemRow.locator('[data-testid="item-photo-thumbnail"]'); await expect(photoThumbnail).toBeVisible({ timeout: 5000 }); // 10. Click photo to open modal and verify full-res photo await photoThumbnail.click(); const photoModal = page.locator('[data-testid="photo-modal"]'); await expect(photoModal).toBeVisible({ timeout: 5000 }); const fullPhoto = photoModal.locator('img'); await expect(fullPhoto).toBeVisible(); // Verify the image has a valid src attribute (should contain /photos/ path) const imgSrc = await fullPhoto.getAttribute('src'); expect(imgSrc).toBeTruthy(); expect(imgSrc).toMatch(/\/photos\/|\/images\/|data:image/); // 11. Close modal by clicking outside or close button const closeButton = photoModal.locator('[data-testid="modal-close"]'); if (await closeButton.isVisible()) { await closeButton.click(); } else { // Click outside the modal await page.click('[data-testid="photo-modal-backdrop"]'); } await expect(photoModal).not.toBeVisible({ timeout: 3000 }); }); test('should display photo metadata in inventory after auto-save', async ({ page }) => { // 1. Capture and extract const captureButton = page.locator('[data-testid="capture-button"]'); await captureButton.click(); const resultsForm = page.locator('[data-testid="extraction-results-form"]'); await expect(resultsForm).toBeVisible({ timeout: 15000 }); // 2. Confirm extraction const confirmButton = page.locator('[data-testid="confirm-extraction"]'); await confirmButton.click(); const successToast = page.locator('[data-testid="toast-success"]'); await expect(successToast).toBeVisible({ timeout: 10000 }); await page.waitForURL(/inventory|items/, { timeout: 10000 }); // 3. Navigate to inventory await page.goto(`${BASE_URL}/inventory`); const inventoryTable = page.locator('[data-testid="inventory-table"]'); await expect(inventoryTable).toBeVisible({ timeout: 5000 }); // 4. Find the newly created item row const firstItemRow = inventoryTable.locator('tbody tr').first(); await expect(firstItemRow).toBeVisible(); // 5. Verify photo column shows photo indicator or date const photoCell = firstItemRow.locator('[data-testid="item-photo-cell"]'); if (await photoCell.isVisible()) { const photoIndicator = photoCell.locator('[data-testid="item-photo-thumbnail"]'); await expect(photoIndicator).toBeVisible(); // Verify photo has visual indication (image or icon) const hasImage = await photoIndicator.locator('img').isVisible(); const hasIcon = await photoIndicator.locator('[role="img"]').isVisible(); expect(hasImage || hasIcon).toBe(true); } }); test('should handle photo upload failure gracefully without blocking item creation', async ({ page, }) => { // Mock photo upload to fail await page.route('**/items/*/photos', (route) => { route.abort('failed'); }); // 1. Capture and extract const captureButton = page.locator('[data-testid="capture-button"]'); await captureButton.click(); const resultsForm = page.locator('[data-testid="extraction-results-form"]'); await expect(resultsForm).toBeVisible({ timeout: 15000 }); // 2. Confirm extraction (photo upload will fail, but item should still be created) const confirmButton = page.locator('[data-testid="confirm-extraction"]'); await confirmButton.click(); // 3. Should show success for item creation (despite photo save failure) const successToast = page.locator('[data-testid="toast-success"]'); await expect(successToast).toBeVisible({ timeout: 10000 }); const toastText = await successToast.textContent(); expect(toastText?.toLowerCase()).toContain('created'); // 4. Should NOT show critical error blocking the operation const errorToast = page.locator('[data-testid="toast-error"]'); const isErrorVisible = await errorToast.isVisible().catch(() => false); // Error about photo might be shown as warning, not blocking error expect(isErrorVisible).toBe(false); await page.waitForURL(/inventory|items/, { timeout: 10000 }); // 5. Navigate to inventory await page.goto(`${BASE_URL}/inventory`); const inventoryTable = page.locator('[data-testid="inventory-table"]'); await expect(inventoryTable).toBeVisible({ timeout: 5000 }); // 6. Verify item still exists without photo const firstItemRow = inventoryTable.locator('tbody tr').first(); await expect(firstItemRow).toBeVisible(); // Photo should be absent or show placeholder const photoThumbnail = firstItemRow.locator('[data-testid="item-photo-thumbnail"]'); const photoVisible = await photoThumbnail.isVisible().catch(() => false); // Photo might not be visible or might show placeholder expect(photoVisible).toBe(false); }); test('should preserve photo even when item data is edited post-save', async ({ page }) => { // 1. Capture and extract const captureButton = page.locator('[data-testid="capture-button"]'); await captureButton.click(); const resultsForm = page.locator('[data-testid="extraction-results-form"]'); await expect(resultsForm).toBeVisible({ timeout: 15000 }); // 2. Get the extracted name for later verification const nameField = page.locator('[data-testid="extracted-name"]'); const originalName = await nameField.inputValue(); // 3. Confirm extraction const confirmButton = page.locator('[data-testid="confirm-extraction"]'); await confirmButton.click(); const successToast = page.locator('[data-testid="toast-success"]'); await expect(successToast).toBeVisible({ timeout: 10000 }); await page.waitForURL(/inventory|items/, { timeout: 10000 }); // 4. Navigate to inventory await page.goto(`${BASE_URL}/inventory`); const inventoryTable = page.locator('[data-testid="inventory-table"]'); await expect(inventoryTable).toBeVisible({ timeout: 5000 }); // 5. Verify photo exists before editing const firstItemRow = inventoryTable.locator('tbody tr').first(); const photoThumbnail = firstItemRow.locator('[data-testid="item-photo-thumbnail"]'); await expect(photoThumbnail).toBeVisible({ timeout: 5000 }); // 6. Click on item to open details/edit page const itemName = firstItemRow.locator('[data-testid="item-name"]'); await itemName.click(); await page.waitForURL(/items\/\d+|inventory\/edit/, { timeout: 10000 }); // 7. Find and verify photo is still visible on detail page const detailPhotoThumbnail = page.locator('[data-testid="item-photo-thumbnail"]'); await expect(detailPhotoThumbnail).toBeVisible({ timeout: 5000 }); // 8. Edit a field (e.g., quantity) const quantityField = page.locator('[data-testid="item-quantity"]'); if (await quantityField.isVisible()) { await quantityField.fill('100'); // Save changes const saveButton = page.locator('[data-testid="save-item-button"]'); if (await saveButton.isVisible()) { await saveButton.click(); const updateToast = page.locator('[data-testid="toast-success"]'); await expect(updateToast).toBeVisible({ timeout: 5000 }); } } // 9. Verify photo still exists after edit const photoAfterEdit = page.locator('[data-testid="item-photo-thumbnail"]'); await expect(photoAfterEdit).toBeVisible({ timeout: 5000 }); }); test('should display photo with correct dimensions in modal', async ({ page }) => { // 1. Capture and extract const captureButton = page.locator('[data-testid="capture-button"]'); await captureButton.click(); const resultsForm = page.locator('[data-testid="extraction-results-form"]'); await expect(resultsForm).toBeVisible({ timeout: 15000 }); // 2. Confirm extraction const confirmButton = page.locator('[data-testid="confirm-extraction"]'); await confirmButton.click(); const successToast = page.locator('[data-testid="toast-success"]'); await expect(successToast).toBeVisible({ timeout: 10000 }); await page.waitForURL(/inventory|items/, { timeout: 10000 }); // 3. Navigate to inventory await page.goto(`${BASE_URL}/inventory`); const inventoryTable = page.locator('[data-testid="inventory-table"]'); await expect(inventoryTable).toBeVisible({ timeout: 5000 }); // 4. Click photo to open modal const firstItemRow = inventoryTable.locator('tbody tr').first(); const photoThumbnail = firstItemRow.locator('[data-testid="item-photo-thumbnail"]'); await photoThumbnail.click(); const photoModal = page.locator('[data-testid="photo-modal"]'); await expect(photoModal).toBeVisible({ timeout: 5000 }); // 5. Verify image is properly rendered const fullPhoto = photoModal.locator('img'); await expect(fullPhoto).toBeVisible(); // 6. Check image dimensions (should be reasonable) const boundingBox = await fullPhoto.boundingBox(); expect(boundingBox).toBeTruthy(); expect(boundingBox!.width).toBeGreaterThan(0); expect(boundingBox!.height).toBeGreaterThan(0); // 7. Verify modal has proper layout (image shouldn't be distorted) const photoContainer = photoModal.locator('[data-testid="photo-container"]'); if (await photoContainer.isVisible()) { const containerBox = await photoContainer.boundingBox(); expect(containerBox).toBeTruthy(); // Image should fit within reasonable bounds expect(containerBox!.width).toBeLessThan(1000); expect(containerBox!.height).toBeLessThan(1000); } }); test('should not show duplicate photos for same item', async ({ page }) => { // 1. Create item with photo const captureButton = page.locator('[data-testid="capture-button"]'); await captureButton.click(); const resultsForm = page.locator('[data-testid="extraction-results-form"]'); await expect(resultsForm).toBeVisible({ timeout: 15000 }); const confirmButton = page.locator('[data-testid="confirm-extraction"]'); await confirmButton.click(); const successToast = page.locator('[data-testid="toast-success"]'); await expect(successToast).toBeVisible({ timeout: 10000 }); await page.waitForURL(/inventory|items/, { timeout: 10000 }); // 2. Navigate to inventory await page.goto(`${BASE_URL}/inventory`); const inventoryTable = page.locator('[data-testid="inventory-table"]'); await expect(inventoryTable).toBeVisible({ timeout: 5000 }); // 3. Verify only one photo thumbnail per item const firstItemRow = inventoryTable.locator('tbody tr').first(); const photoThumbnails = firstItemRow.locator('[data-testid="item-photo-thumbnail"]'); const photoCount = await photoThumbnails.count(); // Should have exactly 1 photo (or 0 if photo save failed) expect(photoCount).toBeLessThanOrEqual(1); }); });