feat: create ai extraction workflow e2e tests
This commit is contained in:
266
frontend/e2e/workflows/3-ai-extraction.spec.ts
Normal file
266
frontend/e2e/workflows/3-ai-extraction.spec.ts
Normal file
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user