test: add E2E test for AI extraction + auto-photo-save flow

This commit is contained in:
2026-04-21 19:33:18 +03:00
parent 3ba31a7b48
commit c22dadbd1a

View 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);
});
});