test: add E2E test for AI extraction + auto-photo-save flow
This commit is contained in:
327
frontend/e2e/workflows/7-ai-extraction-autosave.spec.ts
Normal file
327
frontend/e2e/workflows/7-ai-extraction-autosave.spec.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user