const path = require('path'); const { test, expect } = require('@playwright/test'); const { LIVE_EXPECTED_RACE_STORAGE, LIVE_FACEAI_BASE_URL, LIVE_SITE_BASE_URL, LIVE_SITE_EXPECT_RACE_DATA_AVAILABLE, LIVE_SITE_EXPECT_UNAVAILABLE_REASON_CODE, LIVE_SITE_PORTRAIT_PATH, LIVE_SITE_RACE_ID, LIVE_SITE_RACE_URL, LIVE_SITE_RUN_UPLOAD_FLOW, LIVE_SITE_SAMPLE_PHOTO_IDS, ensureLiveAuthenticatedRacePage, hasExpectedRaceStorage, requirePortraitFixture } = require('./live-site-test-utils'); function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } async function readFaceAiSession(page) { return page.evaluate(async () => { const response = await fetch('/api/session', { credentials: 'include' }); const payload = await response.json().catch(() => null); return { ok: response.ok, status: response.status, payload }; }); } async function openLiveFaceAi(page) { const consoleErrors = []; page.on('console', (message) => { if (message.type() === 'error') { consoleErrors.push(message.text()); } }); await ensureLiveAuthenticatedRacePage(page); await expect(page.locator('h1')).not.toHaveText(/^\s*$/); const launchUrl = await page.evaluate(() => { return typeof buildFaceAiLaunchUrl === 'function' ? buildFaceAiLaunchUrl() : ''; }); expect(launchUrl, 'Expected the legacy race page script to expose a FaceAI handoff URL builder.').toBeTruthy(); const parsedLaunchUrl = new URL(launchUrl, LIVE_SITE_BASE_URL); expect(parsedLaunchUrl.searchParams.get('raceId') || '').toBeTruthy(); expect(parsedLaunchUrl.searchParams.get('returnUrl') || '').toBeTruthy(); if (hasExpectedRaceStorage()) { expect(parsedLaunchUrl.searchParams.get('raceYear')).toBe(LIVE_EXPECTED_RACE_STORAGE.year); expect(parsedLaunchUrl.searchParams.get('raceMonthFolder')).toBe(LIVE_EXPECTED_RACE_STORAGE.monthFolder); expect(parsedLaunchUrl.searchParams.get('raceFolder')).toBe(LIVE_EXPECTED_RACE_STORAGE.raceFolder); expect(parsedLaunchUrl.searchParams.get('raceStorageRelativeDir')).toBe(LIVE_EXPECTED_RACE_STORAGE.relativeDir); } await expect(page.locator('#faceaiLaunchButton')).toBeVisible(); await page.locator('#faceaiLaunchButton').click(); await page.waitForURL(new RegExp(`^${escapeRegExp(LIVE_FACEAI_BASE_URL)}/`), { timeout: 60 * 1000 }); await expect(page.getByRole('heading', { name: /Trova le tue foto con un selfie|Find your photos with a selfie/i })).toBeVisible(); const sessionResponse = await readFaceAiSession(page); expect(sessionResponse.ok, 'Expected the live FaceAI app to expose a readable session payload after launch.').toBe(true); return { consoleErrors, launchUrl: parsedLaunchUrl, session: sessionResponse.payload }; } async function waitForSearchCompletion(page, searchId) { return expect.poll(async () => { return page.evaluate(async (id) => { const response = await fetch(`/api/searches/${id}`, { credentials: 'include' }); if (!response.ok) { return { status: 'poll-error', httpStatus: response.status }; } const payload = await response.json(); return { status: payload.status, errorMessage: payload.errorMessage || null, completionCode: payload.completionCode || null, matchCount: payload.matchCount ?? null, resultId: payload.resultId || null }; }, searchId); }, { timeout: 3 * 60 * 1000, message: `Expected FaceAI search ${searchId} to complete successfully.` }).toMatchObject({ status: 'completed' }); } async function readVisibleLegacyPhotoIds(page) { return page.locator('#demo [data-faceai-photo-id]').evaluateAll((elements) => { return elements.map((element) => String(element.getAttribute('data-faceai-photo-id') || '').trim()).filter(Boolean); }); } async function readFaceAiMatchState(page) { return page.evaluate(() => { if (typeof getFaceAiMatchState === 'function') { return getFaceAiMatchState(); } return window.faceAiMatchState || null; }); } async function waitForVisibleLegacyPhotoIds(page, expectedCount) { await expect.poll(async () => { const visiblePhotoIds = await readVisibleLegacyPhotoIds(page); return visiblePhotoIds.length; }, { timeout: 30 * 1000, message: 'Expected the legacy FaceAI gallery to finish loading matched thumbnails.' }).toBe(expectedCount); return readVisibleLegacyPhotoIds(page); } async function waitForVisibleLegacyThumbs(page, expectedCount) { const thumbs = page.locator('#demo [data-faceai-photo-id] img.thumb'); await expect(thumbs).toHaveCount(expectedCount); await expect.poll(async () => { return thumbs.evaluateAll((elements) => { return elements.every((element) => { const src = String(element.getAttribute('src') || '').trim(); return src.length > 0 && src.indexOf('_imgNotFound') === -1; }); }); }, { timeout: 30 * 1000, message: 'Expected the legacy FaceAI gallery to resolve real thumbnail image URLs.' }).toBe(true); return thumbs; } async function waitForLegacyFaceAiCount(page, expectedCount) { await expect.poll(async () => { return page.locator('#faceAiPhotoCountValue').textContent(); }, { timeout: 30 * 1000, message: 'Expected the legacy FaceAI count to match the resolved gallery size.' }).toBe(String(expectedCount)); } function basenameOfPhotoKey(photoKey) { return String(photoKey).replace(/\\/g, '/').split('/').pop(); } async function lookupLivePhoto(page, photoKey) { return page.evaluate(async (value) => { const lookupData = getFaceAiPhotoLookupData(value); const params = new URLSearchParams(lookupData.request); const response = await fetch(`${getFaceAiLookupEndpoint()}?${params.toString()}`, { credentials: 'include' }); const text = await response.text(); let payload = null; try { payload = JSON.parse(text); } catch (error) { payload = { parseError: String(error), raw: text }; } return { status: response.status, request: lookupData.request, payload }; }, photoKey); } async function expectLegacyFaceAiGalleryToRemainStable(page, expectedPhotoIds, holdMs = 4000) { await page.waitForTimeout(holdMs); const visiblePhotoIds = await readVisibleLegacyPhotoIds(page); expect(visiblePhotoIds.sort()).toEqual(expectedPhotoIds.slice().sort()); await expect(page.locator('#demo [data-faceai-photo-id]')).toHaveCount(expectedPhotoIds.length); await waitForVisibleLegacyThumbs(page, expectedPhotoIds.length); await waitForLegacyFaceAiCount(page, expectedPhotoIds.length); } test('renders the exact live FaceAI filtered sample URL with visible thumbnails', async ({ page }) => { const samplePhotoIds = LIVE_SITE_SAMPLE_PHOTO_IDS; test.skip(!samplePhotoIds.length, 'Set LIVE_SITE_SAMPLE_PHOTO_IDS to validate a race-specific filtered sample URL.'); const sampleUrl = `${LIVE_SITE_RACE_URL}?faceaiMatchSource=faceai&faceaiMatchCount=${samplePhotoIds.length}&faceaiPhotoIds=${encodeURIComponent(samplePhotoIds.join(','))}`; await ensureLiveAuthenticatedRacePage(page); await page.goto(sampleUrl, { waitUntil: 'domcontentloaded' }); await expect(page.locator('form[onsubmit="return searching()"]')).toBeVisible(); await expect(page.locator('#faceAiFilterBanner')).toContainText(/Face ID filter active|Filtro Face ID attivo/i); const visiblePhotoIds = await waitForVisibleLegacyPhotoIds(page, samplePhotoIds.length); expect(visiblePhotoIds.sort()).toEqual(samplePhotoIds.slice().sort()); await waitForVisibleLegacyThumbs(page, samplePhotoIds.length); await waitForLegacyFaceAiCount(page, samplePhotoIds.length); await expectLegacyFaceAiGalleryToRemainStable(page, samplePhotoIds); }); test('keeps the live FaceAI race storage metadata pinned to the stored server path', async ({ page }) => { test.skip(!hasExpectedRaceStorage(), 'Set LIVE_SITE_EXPECTED_RACE_* values to validate exact race storage metadata.'); await ensureLiveAuthenticatedRacePage(page); await expect(page.locator('#faceAiRaceYear')).toHaveValue(LIVE_EXPECTED_RACE_STORAGE.year); await expect(page.locator('#faceAiRaceMonthFolder')).toHaveValue(LIVE_EXPECTED_RACE_STORAGE.monthFolder); await expect(page.locator('#faceAiRaceFolder')).toHaveValue(LIVE_EXPECTED_RACE_STORAGE.raceFolder); await expect(page.locator('#faceAiRaceStorageRelativeDir')).toHaveValue(LIVE_EXPECTED_RACE_STORAGE.relativeDir); const launchUrl = await page.evaluate(() => { return typeof buildFaceAiLaunchUrl === 'function' ? buildFaceAiLaunchUrl() : ''; }); const parsedLaunchUrl = new URL(launchUrl, LIVE_SITE_BASE_URL); expect(parsedLaunchUrl.searchParams.get('raceYear')).toBe(LIVE_EXPECTED_RACE_STORAGE.year); expect(parsedLaunchUrl.searchParams.get('raceMonthFolder')).toBe(LIVE_EXPECTED_RACE_STORAGE.monthFolder); expect(parsedLaunchUrl.searchParams.get('raceFolder')).toBe(LIVE_EXPECTED_RACE_STORAGE.raceFolder); expect(parsedLaunchUrl.searchParams.get('raceStorageRelativeDir')).toBe(LIVE_EXPECTED_RACE_STORAGE.relativeDir); }); test('resolves the live sample photo lookups inside the current race', async ({ page }) => { const samplePhotoIds = LIVE_SITE_SAMPLE_PHOTO_IDS; test.skip(!samplePhotoIds.length, 'Set LIVE_SITE_SAMPLE_PHOTO_IDS to validate race-specific photo lookups.'); await ensureLiveAuthenticatedRacePage(page); for (const photoKey of samplePhotoIds) { const lookup = await lookupLivePhoto(page, photoKey); expect(lookup.status, `Expected faceai_photo_lookup.jsp to resolve ${photoKey} on the live Firenze race page.`).toBe(200); expect(String(lookup.request.raceId || '')).toBe(LIVE_SITE_RACE_ID); expect(lookup.payload && lookup.payload.found, `Expected ${photoKey} to resolve within the current live race.`).toBe(true); expect(String(lookup.payload.photoId || '')).toBe(photoKey); expect(basenameOfPhotoKey(String(lookup.payload.resolvedFile || ''))).toBe(basenameOfPhotoKey(photoKey)); expect(String(lookup.payload.thumbSrc || '')).toContain(`+tn-${lookup.payload.legacyId}.jpg`); } }); test('loads a live race page with an authenticated session', async ({ page }) => { await ensureLiveAuthenticatedRacePage(page); const cookies = await page.context().cookies(LIVE_SITE_RACE_URL); const faceAiIdentityCookie = cookies.find((cookie) => cookie.name === 'rus_faceai_identity'); expect(faceAiIdentityCookie, 'Expected the race page to mint the FaceAI identity cookie for the authenticated session.').toBeTruthy(); expect(faceAiIdentityCookie.httpOnly).toBe(true); expect(faceAiIdentityCookie.secure).toBe(true); expect(faceAiIdentityCookie.value).toMatch(/\./); }); test('launches the live FaceAI app with race storage metadata and a styled header', async ({ page }) => { const { consoleErrors, launchUrl, session } = await openLiveFaceAi(page); expect(launchUrl.searchParams.get('raceId')).toBe(LIVE_SITE_RACE_ID); expect(launchUrl.searchParams.get('raceStorageRelativeDir') || '').toBeTruthy(); await expect(page.locator('nav.navbar')).toBeVisible(); await expect(page.locator('link[data-legacy-href*="bootstrap.min.css"]')).toHaveCount(1); await expect(page.locator('link[data-legacy-href*="custom-style.css"]')).toHaveCount(1); await expect(page.locator('link[data-legacy-href*="font-awesome"]')).toHaveCount(0); const legacyStylesheetHrefs = await page.locator('link[data-legacy-href]').evaluateAll((elements) => { return elements.map((element) => element.getAttribute('href') || ''); }); expect(legacyStylesheetHrefs.some((href) => href.startsWith(`${LIVE_SITE_BASE_URL}/`))).toBe(true); const navComputedStyles = await page.locator('nav.navbar').evaluate((element) => { const styles = window.getComputedStyle(element); return { position: styles.position, display: styles.display }; }); expect(navComputedStyles.position).toBe('fixed'); expect(navComputedStyles.display).not.toBe('block'); if (LIVE_SITE_EXPECT_RACE_DATA_AVAILABLE) { expect(session?.availability?.available).toBe(true); expect(consoleErrors.find((entry) => entry.includes('MISSING_RACE_STORAGE')) || '').toBe(''); expect(consoleErrors.find((entry) => entry.includes('[FaceAI] Invalid race data:')) || '').toBe(''); await expect(page.locator('input[type="file"]')).toBeEnabled(); } else { expect(session?.availability?.available).toBe(false); expect(session?.availability?.reasonCode).toBe(LIVE_SITE_EXPECT_UNAVAILABLE_REASON_CODE); await expect(page.locator('input[type="file"]')).toBeDisabled(); await expect(page.locator('.faceai-feedback')).toContainText(/FaceAI data is not available for this race|I dati FaceAI non sono disponibili per questa gara|The race data received for this session is invalid/i); } }); test('returns to the live race page from FaceAI without leaving the legacy spinner stuck', async ({ page }) => { await openLiveFaceAi(page); await page.getByRole('button', { name: /Torna alla pagina gara|Back to the race page/i }).click(); await page.waitForURL((url) => { return url.toString().startsWith(LIVE_SITE_BASE_URL) && !url.toString().startsWith(LIVE_FACEAI_BASE_URL); }, { timeout: 60 * 1000, waitUntil: 'commit' }); await expect(page.locator('form[onsubmit="return searching()"]')).toBeVisible(); await expect(page.locator('#faceaiLaunchButton')).toBeVisible(); const bodyState = await page.locator('body').evaluate((element) => { return { className: element.className, ariaBusy: element.getAttribute('aria-busy') }; }); expect(bodyState.className).not.toContain('loading'); expect(bodyState.ariaBusy).not.toBe('true'); }); test('accepts the supplied portrait image for the live upload flow', async ({ page }) => { test.slow(); test.skip(!LIVE_SITE_RUN_UPLOAD_FLOW, 'Set LIVE_SITE_RUN_UPLOAD_FLOW=1 to exercise the live upload flow.'); test.skip(!LIVE_SITE_EXPECT_RACE_DATA_AVAILABLE, 'Set LIVE_SITE_EXPECT_RACE_DATA_AVAILABLE=1 when the target race has FaceAI data available.'); requirePortraitFixture(); await openLiveFaceAi(page); const fileInput = page.locator('input[type="file"]'); await expect(fileInput).toBeEnabled(); await fileInput.setInputFiles(LIVE_SITE_PORTRAIT_PATH); await expect(page.locator('.faceai-file-name')).toContainText(path.basename(LIVE_SITE_PORTRAIT_PATH)); const searchResponsePromise = page.waitForResponse((response) => { return response.request().method() === 'POST' && response.url().includes('/api/searches'); }, { timeout: 60 * 1000 }); await page.getByRole('button', { name: /Avvia ricerca Face ID|Start Face ID search/i }).click(); const searchResponse = await searchResponsePromise; expect(searchResponse.ok(), 'Expected the live upload flow to create a FaceAI search successfully.').toBe(true); const searchPayload = await searchResponse.json(); const searchId = searchPayload.id || searchPayload.searchId; expect(searchId, 'Expected the search creation response to include a search identifier.').toBeTruthy(); const completion = await page.evaluate(async (id) => { const response = await fetch(`/api/searches/${id}`, { credentials: 'include' }); return response.json(); }, searchId).catch(() => null); if (completion?.status === 'failed') { throw new Error(`FaceAI search ${searchId} failed immediately: ${completion.errorMessage || 'unknown error'}`); } await waitForSearchCompletion(page, searchId); await expect.poll(async () => page.url(), { timeout: 30 * 1000, message: 'Expected the browser to land on the legacy race page with FaceAI filter parameters after FaceAI completed.' }).toMatch(new RegExp(`^${escapeRegExp(LIVE_SITE_BASE_URL)}/.*(?:faceaiPhotoIds=|faceaiMatchStorageKey=)`)); await expect(page.locator('form[onsubmit="return searching()"]')).toBeVisible(); await expect(page.locator('#faceAiFilterBanner')).toContainText(/Face ID filter active|Filtro Face ID attivo/i); await expect(page.locator('.gallery-card')).toHaveCount(0); const finalUrl = new URL(page.url()); await expect.poll(async () => { const matchState = await readFaceAiMatchState(page); return Array.isArray(matchState && matchState.photoIds) ? matchState.photoIds.length : 0; }, { timeout: 30 * 1000, message: 'Expected the legacy race page to resolve FaceAI match state after redirect.' }).toBeGreaterThan(0); const matchState = await readFaceAiMatchState(page); const expectedPhotoIds = Array.isArray(matchState.photoIds) ? matchState.photoIds.map((value) => String(value || '').trim()).filter(Boolean) : []; expect(expectedPhotoIds.length, 'Expected the final race page URL to include at least one FaceAI photo identifier.').toBeGreaterThan(0); expect(Number(finalUrl.searchParams.get('faceaiMatchCount') || 0)).toBe(expectedPhotoIds.length); for (const photoKey of expectedPhotoIds) { const lookup = await lookupLivePhoto(page, photoKey); expect(lookup.status, `Expected FaceAI to return a photo key that resolves on the current live race page: ${photoKey}`).toBe(200); expect(lookup.payload && lookup.payload.found, `Expected FaceAI to return a photo key that exists in the current live race: ${photoKey}`).toBe(true); } const visiblePhotoIds = await waitForVisibleLegacyPhotoIds(page, expectedPhotoIds.length); expect(visiblePhotoIds.length, 'Expected at least one legacy race thumbnail to remain visible after FaceAI filtering.').toBeGreaterThan(0); expect(visiblePhotoIds.sort()).toEqual(expectedPhotoIds.slice().sort()); await waitForVisibleLegacyThumbs(page, expectedPhotoIds.length); await waitForLegacyFaceAiCount(page, expectedPhotoIds.length); await expectLegacyFaceAiGalleryToRemainStable(page, expectedPhotoIds); });