diff --git a/frontend/e2e/workflows/3-ai-extraction.spec.ts b/frontend/e2e/workflows/3-ai-extraction.spec.ts new file mode 100644 index 00000000..99218e17 --- /dev/null +++ b/frontend/e2e/workflows/3-ai-extraction.spec.ts @@ -0,0 +1,266 @@ +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 Workflow', () => { + const BASE_URL = process.env.BASE_URL || 'http://localhost:3000'; + + 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`); + }); + + test('should display AI onboarding wizard', async ({ page }) => { + const aiWizard = page.locator('[data-testid="ai-onboarding-wizard"]'); + await expect(aiWizard).toBeVisible(); + + // Check for step indicators + const stepIndicator = page.locator('[data-testid="wizard-step"]'); + expect(await stepIndicator.count()).toBeGreaterThan(0); + }); + + test('should show capture step in AI wizard', async ({ page }) => { + const captureStep = page.locator('[data-testid="wizard-step-capture"]'); + await expect(captureStep).toBeVisible(); + + // Camera view should be displayed + const cameraView = page.locator('[data-testid="capture-camera"]'); + await expect(cameraView).toBeVisible(); + + // Capture button should be visible + const captureButton = page.locator('[data-testid="capture-button"]'); + await expect(captureButton).toBeVisible(); + }); + + test('should allow manual item entry as alternative to AI', async ({ page }) => { + const manualEntryTab = page.locator('[data-testid="manual-entry-tab"]'); + if (await manualEntryTab.isVisible()) { + await manualEntryTab.click(); + + // Form should be displayed + const itemForm = page.locator('[data-testid="item-manual-form"]'); + await expect(itemForm).toBeVisible(); + } + }); + + test('should handle camera permission denied', async ({ page, context }) => { + // Simulate camera permission denial + await context.grantPermissions([]); + + // Reload to trigger permission check + await page.reload(); + + // Should show error or fallback UI + const errorMessage = page.locator('[data-testid="camera-permission-error"]'); + const fallbackForm = page.locator('[data-testid="manual-entry-form"]'); + + const hasError = await errorMessage.isVisible().catch(() => false); + const hasFallback = await fallbackForm.isVisible().catch(() => false); + + expect(hasError || hasFallback).toBe(true); + }); + + test('should display AI provider selection', async ({ page }) => { + const providerSelect = page.locator('[data-testid="ai-provider-select"]'); + if (await providerSelect.isVisible()) { + // At least Gemini or Claude should be available + const options = page.locator('[data-testid="provider-option"]'); + expect(await options.count()).toBeGreaterThan(0); + } + }); + + test('should show extraction results after AI processing', async ({ page }) => { + // Click capture button (mock will return predefined response) + const captureButton = page.locator('[data-testid="capture-button"]'); + await captureButton.click(); + + // Show extraction overlay + const extractionOverlay = page.locator('[data-testid="ai-extraction-overlay"]'); + await expect(extractionOverlay).toBeVisible({ timeout: 10000 }); + + // Results form should appear + const resultsForm = page.locator('[data-testid="extraction-results-form"]'); + await expect(resultsForm).toBeVisible({ timeout: 15000 }); + }); + + test('should display extracted fields for user confirmation', async ({ page }) => { + const captureButton = page.locator('[data-testid="capture-button"]'); + await captureButton.click(); + + // Wait for results + const resultsForm = page.locator('[data-testid="extraction-results-form"]'); + await expect(resultsForm).toBeVisible({ timeout: 15000 }); + + // Check for extracted fields + const nameField = page.locator('[data-testid="extracted-name"]'); + const categoryField = page.locator('[data-testid="extracted-category"]'); + + await expect(nameField).toBeVisible(); + if (await categoryField.isVisible()) { + // Category might be optional + } + }); + + test('should allow editing extracted values', async ({ page }) => { + const captureButton = page.locator('[data-testid="capture-button"]'); + await captureButton.click(); + + // Wait for results + const resultsForm = page.locator('[data-testid="extraction-results-form"]'); + await expect(resultsForm).toBeVisible({ timeout: 15000 }); + + // Edit extracted name + const nameField = page.locator('[data-testid="extracted-name"]'); + await nameField.fill('Modified Item Name'); + + const value = await helpers.getInputValue(page, '[data-testid="extracted-name"]'); + expect(value).toBe('Modified Item Name'); + }); + + test('should allow confirming and saving extracted item', async ({ page }) => { + const captureButton = page.locator('[data-testid="capture-button"]'); + await captureButton.click(); + + // Wait for results + const resultsForm = page.locator('[data-testid="extraction-results-form"]'); + await expect(resultsForm).toBeVisible({ timeout: 15000 }); + + // Confirm extraction + const confirmButton = page.locator('[data-testid="confirm-extraction"]'); + await confirmButton.click(); + + // Should show success and redirect + const successToast = page.locator('[data-testid="toast-success"]'); + await expect(successToast).toBeVisible({ timeout: 5000 }); + + // Should redirect to inventory or item detail page + await page.waitForURL(/inventory|items/, { timeout: 5000 }); + }); + + test('should allow rejecting AI extraction results', async ({ page }) => { + const captureButton = page.locator('[data-testid="capture-button"]'); + await captureButton.click(); + + // Wait for results + const resultsForm = page.locator('[data-testid="extraction-results-form"]'); + await expect(resultsForm).toBeVisible({ timeout: 15000 }); + + // Click reject/redo + const rejectButton = page.locator('[data-testid="reject-extraction"]'); + if (await rejectButton.isVisible()) { + await rejectButton.click(); + + // Should return to capture step + const captureStep = page.locator('[data-testid="wizard-step-capture"]'); + await expect(captureStep).toBeVisible({ timeout: 3000 }); + } + }); + + test('should handle AI extraction errors gracefully', async ({ page }) => { + // Mock API error + await page.route('**/api/ai/extract', (route) => { + route.abort('failed'); + }); + + const captureButton = page.locator('[data-testid="capture-button"]'); + await captureButton.click(); + + // Should show error message + const errorToast = page.locator('[data-testid="toast-error"]'); + await expect(errorToast).toBeVisible({ timeout: 10000 }); + }); + + test('should support multi-item extraction from single image', async ({ page }) => { + // If multiple items detected feature is supported + const multiItemToggle = page.locator('[data-testid="multi-item-toggle"]'); + if (await multiItemToggle.isVisible()) { + await multiItemToggle.click(); + + const captureButton = page.locator('[data-testid="capture-button"]'); + await captureButton.click(); + + // Results should show multiple items + const itemRows = page.locator('[data-testid="extraction-item-row"]'); + const count = await itemRows.count(); + expect(count).toBeGreaterThanOrEqual(1); + } + }); + + test('should display confidence scores for extracted fields', async ({ page }) => { + const captureButton = page.locator('[data-testid="capture-button"]'); + await captureButton.click(); + + // Wait for results + const resultsForm = page.locator('[data-testid="extraction-results-form"]'); + await expect(resultsForm).toBeVisible({ timeout: 15000 }); + + // Check for confidence indicator + const confidenceIndicator = page.locator('[data-testid="field-confidence"]'); + if (await confidenceIndicator.isVisible()) { + const score = await confidenceIndicator.textContent(); + expect(score).toMatch(/\d+%|High|Medium|Low/); + } + }); + + test('should allow retaking capture', async ({ page }) => { + // Mock to get to results + 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 }); + + // Click retake button + const retakeButton = page.locator('[data-testid="retake-button"]'); + if (await retakeButton.isVisible()) { + await retakeButton.click(); + + // Should return to camera + const captureStep = page.locator('[data-testid="wizard-step-capture"]'); + await expect(captureStep).toBeVisible({ timeout: 3000 }); + } + }); + + test('should validate required fields before submission', async ({ page }) => { + 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 }); + + // Clear required field + const nameField = page.locator('[data-testid="extracted-name"]'); + await nameField.fill(''); + + // Try to confirm + const confirmButton = page.locator('[data-testid="confirm-extraction"]'); + if (await confirmButton.isEnabled()) { + await confirmButton.click(); + + // Should show validation error + const errorToast = page.locator('[data-testid="toast-error"]'); + await expect(errorToast).toBeVisible({ timeout: 3000 }); + } + }); + + test('should show step progress indicator', async ({ page }) => { + const progressBar = page.locator('[data-testid="wizard-progress"]'); + if (await progressBar.isVisible()) { + const currentStep = page.locator('[data-testid="current-step"]'); + const totalSteps = page.locator('[data-testid="total-steps"]'); + + const current = await currentStep.textContent(); + const total = await totalSteps.textContent(); + + expect(current).toBeTruthy(); + expect(total).toBeTruthy(); + } + }); +});