feat: Update FaceAI upload panel and improve race storage metadata handling in tests
All checks were successful
Publish FaceAI Container / publish (push) Successful in 4m32s

This commit is contained in:
MaddoScientisto 2026-04-20 00:11:03 +02:00
commit c0d072c6ea
6 changed files with 130 additions and 47 deletions

View file

@ -46,8 +46,8 @@ const props = defineProps({
type: Boolean,
required: true
},
fileInput: {
type: Object,
assignFileInput: {
type: Function,
required: true
},
t: {
@ -56,10 +56,6 @@ const props = defineProps({
}
});
function assignFileInput(element) {
props.fileInput.value = element;
}
const emit = defineEmits([
'open-file-picker',
'file-change',

View file

@ -116,7 +116,7 @@ const knownServerCodes = {
RATE_LIMITED: 'rateLimited',
MISSING_SELFIE: 'chooseSelfie',
RACE_PKL_UNAVAILABLE: 'raceDataUnavailable',
RACE_DIRECTORY_NOT_FOUND: 'invalidRaceData',
RACE_DIRECTORY_NOT_FOUND: 'raceDataUnavailable',
MISSING_RACE_STORAGE: 'invalidRaceData'
};
@ -138,7 +138,7 @@ const simulatorUrl = legacyUrl('/faceai_simulator.php?raceId=101&lang=it');
const legacyHomeUrl = legacyUrl('/');
function isInvalidRaceAvailability(availability) {
return availability?.reasonCode === 'RACE_DIRECTORY_NOT_FOUND' || availability?.reasonCode === 'MISSING_RACE_STORAGE';
return availability?.reasonCode === 'MISSING_RACE_STORAGE';
}
function buildLegacyReturnUrl(url) {
@ -204,8 +204,9 @@ export function useFaceAiHome() {
}
function getAvailabilityUserMessage(availability, fallbackKey = 'unavailableDefault') {
if (isInvalidRaceAvailability(availability)) {
return t('invalidRaceData');
const mappedCodeKey = availability?.reasonCode ? knownServerCodes[availability.reasonCode] : null;
if (mappedCodeKey) {
return t(mappedCodeKey);
}
return localizeServerMessage(availability?.message, fallbackKey);

View file

@ -35,6 +35,10 @@ const {
submitSearch,
t
} = useFaceAiHome();
function assignFileInput(element) {
fileInput.value = element;
}
</script>
<template>
@ -63,7 +67,7 @@ const {
:selected-file="selectedFile"
:selected-file-size-label="selectedFileSizeLabel"
:can-start-search="canStartSearch"
:file-input="fileInput"
:assign-file-input="assignFileInput"
:t="t"
@open-file-picker="openFilePicker"
@file-change="onFileChange"

View file

@ -201,6 +201,13 @@ test('builds the simulator FaceAI handoff URL with the exact local race storage
});
test('shows the unsupported-race message when the current race has no PKL data and lets the user go back', async ({ page }) => {
const consoleErrors = [];
page.on('console', (message) => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
}
});
await launchFromSimulator(page, {
raceId: '404',
raceSlug: 'corsa-di-livorno',
@ -211,6 +218,9 @@ test('shows the unsupported-race message when the current race has no PKL data a
await expect(page.locator('.faceai-feedback')).toContainText('FaceAI non è disponibile per questa gara.');
await expect(page.locator('input[type="file"]')).toBeDisabled();
await expect(page.getByRole('button', { name: 'Scegli immagine' })).toBeDisabled();
await expect.poll(() => {
return consoleErrors.find((entry) => entry.includes('[FaceAI] Invalid race data:')) || null;
}).toBeNull();
await page.waitForTimeout(2000);
await expect(page).toHaveURL(FACEAI_HOME_URL_RE);
@ -219,7 +229,7 @@ test('shows the unsupported-race message when the current race has no PKL data a
await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('404'));
});
test('shows a localized invalid-race error when session race data points to a missing folder', async ({ page }) => {
test('shows a localized invalid-race error when the handoff omits race storage metadata', async ({ page }) => {
const consoleErrors = [];
page.on('console', (message) => {
if (message.type() === 'error') {
@ -227,17 +237,18 @@ test('shows a localized invalid-race error when session race data points to a mi
}
});
const simulatorUrl = buildSimulatorUrl({
const handoffUrl = new URL(buildHandoffUrl({
raceId: '405',
lang: 'en',
raceSlug: 'ghost-race',
raceName: 'Ghost Race',
raceFolder: 'THIS RACE DOES NOT EXIST'
});
}));
handoffUrl.searchParams.delete('raceYear');
handoffUrl.searchParams.delete('raceMonthFolder');
handoffUrl.searchParams.delete('raceFolder');
await page.goto(simulatorUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('#faceaiLaunchButton')).toBeVisible();
await page.locator('#faceaiLaunchButton').click();
await page.goto(handoffUrl.toString(), { waitUntil: 'domcontentloaded' });
await page.waitForURL(FACEAI_HOME_URL_RE, {
timeout: SHORT_UI_TIMEOUT_MS
@ -250,10 +261,10 @@ test('shows a localized invalid-race error when session race data points to a mi
await expect(page.getByRole('button', { name: 'Back to the race page' })).toBeVisible();
await expect.poll(() => {
return consoleErrors.find((entry) => entry.includes('[FaceAI] Invalid race data:')) || null;
}).toContain('RACE_DIRECTORY_NOT_FOUND');
}).toContain('MISSING_RACE_STORAGE');
await expect.poll(() => {
return consoleErrors.find((entry) => entry.includes('[FaceAI] Invalid race data:')) || null;
}).toContain('THIS RACE DOES NOT EXIST');
}).toContain('MISSING_RACE_STORAGE');
await page.getByRole('button', { name: 'Back to the race page' }).click();
await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('405'));

View file

@ -1,30 +1,36 @@
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');
const LIVE_EXPECTED_RACE_STORAGE = {
year: '2026',
monthFolder: '03.MARZO',
raceFolder: 'HMF_2026',
relativeDir: '2026/03.MARZO/HMF_2026'
};
const LIVE_SAMPLE_PHOTO_IDS = [
'21.ARRIVO_CON TEMPO\\DSC_8385.JPG',
'21.ARRIVO_CON TEMPO\\DSC_8295.JPG'
];
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 = [];
@ -35,7 +41,7 @@ async function openLiveFaceAi(page) {
});
await ensureLiveAuthenticatedRacePage(page);
await expect(page.locator('h1')).toContainText(/HALF MARATHON FIRENZE|Competitions|Gare/i);
await expect(page.locator('h1')).not.toHaveText(/^\s*$/);
const launchUrl = await page.evaluate(() => {
return typeof buildFaceAiLaunchUrl === 'function' ? buildFaceAiLaunchUrl() : '';
@ -44,10 +50,15 @@ async function openLiveFaceAi(page) {
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();
@ -57,9 +68,13 @@ async function openLiveFaceAi(page) {
});
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
launchUrl: parsedLaunchUrl,
session: sessionResponse.payload
};
}
@ -180,8 +195,10 @@ async function expectLegacyFaceAiGalleryToRemainStable(page, expectedPhotoIds, h
}
test('renders the exact live FaceAI filtered sample URL with visible thumbnails', async ({ page }) => {
const samplePhotoIds = LIVE_SAMPLE_PHOTO_IDS;
const sampleUrl = `${LIVE_SITE_BASE_URL}/42%20HALF%20MARATHON%20FIRENZE_gara-1018545---48-1.html?faceaiMatchSource=faceai&faceaiMatchCount=2&faceaiPhotoIds=${encodeURIComponent(samplePhotoIds.join(','))}`;
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, {
@ -199,7 +216,9 @@ test('renders the exact live FaceAI filtered sample URL with visible thumbnails'
await expectLegacyFaceAiGalleryToRemainStable(page, samplePhotoIds);
});
test('keeps the live Firenze FaceAI race storage metadata pinned to the stored server path', async ({ page }) => {
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);
@ -218,15 +237,16 @@ test('keeps the live Firenze FaceAI race storage metadata pinned to the stored s
expect(parsedLaunchUrl.searchParams.get('raceStorageRelativeDir')).toBe(LIVE_EXPECTED_RACE_STORAGE.relativeDir);
});
test('resolves the live Firenze sample photo lookups inside the current race', async ({ page }) => {
const samplePhotoIds = LIVE_SAMPLE_PHOTO_IDS;
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('1018545');
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));
@ -247,9 +267,10 @@ test('loads a live race page with an authenticated session', async ({ page }) =>
});
test('launches the live FaceAI app with race storage metadata and a styled header', async ({ page }) => {
const { consoleErrors, launchUrl } = await openLiveFaceAi(page);
const { consoleErrors, launchUrl, session } = await openLiveFaceAi(page);
expect(launchUrl.searchParams.get('raceStorageRelativeDir')).toBe(LIVE_EXPECTED_RACE_STORAGE.relativeDir);
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);
@ -271,8 +292,18 @@ test('launches the live FaceAI app with race storage metadata and a styled heade
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 }) => {
@ -304,6 +335,7 @@ test('returns to the live race page from FaceAI without leaving the legacy spinn
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();

View file

@ -15,8 +15,40 @@ const LIVE_SITE_USERNAME = process.env.LIVE_SITE_USERNAME || '';
const LIVE_SITE_PASSWORD = process.env.LIVE_SITE_PASSWORD || '';
const LIVE_SITE_PORTRAIT_PATH = process.env.LIVE_SITE_PORTRAIT_PATH || path.join(WORKSPACE_ROOT, 'test_pkl', 'live', 'test_portrait_1.png');
const LIVE_SITE_RUN_UPLOAD_FLOW = process.env.LIVE_SITE_RUN_UPLOAD_FLOW === '1';
const LIVE_SITE_EXPECT_RACE_DATA_AVAILABLE = process.env.LIVE_SITE_EXPECT_RACE_DATA_AVAILABLE !== '0';
const LIVE_SITE_EXPECT_UNAVAILABLE_REASON_CODE = process.env.LIVE_SITE_EXPECT_UNAVAILABLE_REASON_CODE || 'RACE_DIRECTORY_NOT_FOUND';
const AUTH_FILE = path.join(__dirname, '.auth', 'user.json');
function parseOptionalCsv(value) {
return String(value || '')
.split(',')
.map((entry) => entry.trim())
.filter(Boolean);
}
function parseRaceIdFromUrl(value) {
const match = String(value || '').match(/_gara-(\d+)---/i);
return match ? match[1] : '';
}
const LIVE_SITE_RACE_ID = process.env.LIVE_SITE_RACE_ID || parseRaceIdFromUrl(LIVE_SITE_RACE_URL);
const LIVE_EXPECTED_RACE_STORAGE = {
year: String(process.env.LIVE_SITE_EXPECTED_RACE_YEAR || '').trim(),
monthFolder: String(process.env.LIVE_SITE_EXPECTED_RACE_MONTH_FOLDER || '').trim(),
raceFolder: String(process.env.LIVE_SITE_EXPECTED_RACE_FOLDER || '').trim(),
relativeDir: String(process.env.LIVE_SITE_EXPECTED_RACE_STORAGE_RELATIVE_DIR || '').trim()
};
const LIVE_SITE_SAMPLE_PHOTO_IDS = parseOptionalCsv(process.env.LIVE_SITE_SAMPLE_PHOTO_IDS);
function hasExpectedRaceStorage() {
return Boolean(
LIVE_EXPECTED_RACE_STORAGE.year
&& LIVE_EXPECTED_RACE_STORAGE.monthFolder
&& LIVE_EXPECTED_RACE_STORAGE.raceFolder
&& LIVE_EXPECTED_RACE_STORAGE.relativeDir
);
}
function ensureAuthDirectory() {
fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true });
}
@ -155,19 +187,26 @@ function requirePortraitFixture() {
module.exports = {
AUTH_FILE,
LIVE_FACEAI_BASE_URL,
LIVE_EXPECTED_RACE_STORAGE,
LIVE_SITE_BASE_URL,
LIVE_SITE_LOGIN_URL,
LIVE_SITE_PASSWORD,
LIVE_SITE_PORTRAIT_PATH,
LIVE_SITE_EXPECT_RACE_DATA_AVAILABLE,
LIVE_SITE_EXPECT_UNAVAILABLE_REASON_CODE,
LIVE_SITE_RACE_ID,
LIVE_SITE_RACE_URL,
LIVE_SITE_RESULT_URL_PATTERN,
LIVE_SITE_RUN_UPLOAD_FLOW,
LIVE_SITE_SAMPLE_PHOTO_IDS,
LIVE_SITE_USERNAME,
dismissCookieBanner,
ensureLiveAuthenticatedRacePage,
ensureAuthDirectory,
expectRacePageLoaded,
hasExpectedRaceStorage,
loginSubmitLocator,
parseRaceIdFromUrl,
performLiveLogin,
performLiveLoginRequest,
requirePortraitFixture,