All checks were successful
Publish FaceAI Container / publish (push) Successful in 5m45s
424 lines
18 KiB
JavaScript
424 lines
18 KiB
JavaScript
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);
|
|
});
|