267 lines
10 KiB
TypeScript
267 lines
10 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|