Enhance FaceAI integration with live checks and metadata handling
All checks were successful
Publish FaceAI Container / publish (push) Successful in 3m22s

- Added optional live FaceAI checks in README.md
- Implemented relative storage segment parsing in race-storage.js
- Updated server.js to include relative directory in race storage
- Refactored legacyAssets.js to resolve asset base URL dynamically
- Expanded live race tests to validate FaceAI app launch and metadata
- Introduced portrait image handling for live upload flow
- Updated faceai_handoff.php to process race storage relative directory
This commit is contained in:
MaddoScientisto 2026-04-19 10:38:32 +02:00
commit bb60201ad4
9 changed files with 214 additions and 25 deletions

View file

@ -201,6 +201,21 @@ What it does:
- opens the configured live race URL
- verifies the account UI is present and the race search form renders correctly
Optional live FaceAI checks can also be enabled with:
```bash
LIVE_FACEAI_BASE_URL=https://ai.regalamiunsorriso.it
LIVE_SITE_PORTRAIT_PATH=../test_pkl/live/test_portrait_1.png
LIVE_SITE_RUN_UPLOAD_FLOW=1
```
When enabled, the live suite also:
- validates that the legacy Face ID handoff URL includes the race storage metadata expected by FaceAI
- opens the real FaceAI app and asserts that the legacy header stylesheets load from the live legacy site
- confirms the app does not emit the `MISSING_RACE_STORAGE` invalid-race error on launch
- uploads the supplied portrait image and verifies that search creation succeeds
## Optional Backend And Frontend Dev Loop
If you only want to iterate on the app without the PHP simulator, you can still run the public site and the processor separately. The queue-backed flow now requires Redis and the processor, so `npm run dev` alone is no longer the full stack.

View file

@ -30,6 +30,20 @@ function sanitizePathSegment(value) {
return normalized;
}
function parseRelativeStorageSegments(value) {
const normalized = String(value || '').trim().replace(/\\/g, '/');
if (!normalized) {
return [];
}
return normalized
.split('/')
.map((segment) => segment.trim())
.filter(Boolean)
.map((segment) => sanitizePathSegment(segment));
}
export function normalizeRaceFolderName(value) {
return String(value || '')
.trim()
@ -49,9 +63,16 @@ export function buildMonthFolder(year, monthIndex) {
}
export function buildRaceStorage(storageInput = {}) {
const year = sanitizePathSegment(storageInput.year);
const monthFolder = sanitizePathSegment(storageInput.monthFolder);
const raceFolder = sanitizePathSegment(normalizeRaceFolderName(storageInput.raceFolder));
const relativeSegments = parseRelativeStorageSegments(storageInput.relativeDir);
const relativeYear = relativeSegments[0] || '';
const relativeMonthFolder = relativeSegments.length >= 3 ? relativeSegments[1] : '';
const relativeRaceFolder = relativeSegments.length >= 3
? relativeSegments[2]
: (relativeSegments.length >= 2 ? relativeSegments[1] : '');
const year = sanitizePathSegment(storageInput.year || relativeYear);
const monthFolder = sanitizePathSegment(storageInput.monthFolder || relativeMonthFolder);
const raceFolder = sanitizePathSegment(normalizeRaceFolderName(storageInput.raceFolder || relativeRaceFolder));
if (!year || !monthFolder || !raceFolder) {
return null;

View file

@ -262,7 +262,8 @@ app.get('/dev/legacy/launch', (req, res) => {
raceStorage: {
year: String(req.query.raceYear || mockCatalog[raceId]?.storage?.year || ''),
monthFolder: String(req.query.raceMonthFolder || mockCatalog[raceId]?.storage?.monthFolder || ''),
raceFolder: String(req.query.raceFolder || mockCatalog[raceId]?.storage?.raceFolder || '')
raceFolder: String(req.query.raceFolder || mockCatalog[raceId]?.storage?.raceFolder || ''),
relativeDir: String(req.query.raceStorageRelativeDir || '')
},
lang,
returnUrl

View file

@ -1,4 +1,33 @@
const legacyAssetBaseUrl = (import.meta.env.VITE_LEGACY_ASSET_BASE_URL || '/legacy-static').replace(/\/$/, '');
import { getLegacyBaseUrl } from './legacyUrls.js';
const localHostnames = new Set(['localhost', '127.0.0.1', '::1']);
function trimTrailingSlash(value) {
return String(value || '').replace(/\/$/, '');
}
function currentHostname() {
if (typeof window === 'undefined' || !window.location || !window.location.hostname) {
return '';
}
return window.location.hostname.toLowerCase();
}
function resolveLegacyAssetBaseUrl() {
const configuredAssetBaseUrl = trimTrailingSlash(import.meta.env.VITE_LEGACY_ASSET_BASE_URL || '');
if (configuredAssetBaseUrl) {
return configuredAssetBaseUrl;
}
if (localHostnames.has(currentHostname())) {
return '/legacy-static';
}
return getLegacyBaseUrl();
}
const legacyAssetBaseUrl = resolveLegacyAssetBaseUrl();
export function legacyAsset(path) {
return `${legacyAssetBaseUrl}${path.startsWith('/') ? path : `/${path}`}`;

View file

@ -1,28 +1,59 @@
const { test, expect } = require('@playwright/test');
const {
LIVE_FACEAI_BASE_URL,
LIVE_SITE_BASE_URL,
LIVE_SITE_PORTRAIT_PATH,
LIVE_SITE_RACE_URL,
dismissCookieBanner,
expectRacePageLoaded,
performLiveLogin,
waitForLoggedInUi
LIVE_SITE_RUN_UPLOAD_FLOW,
ensureLiveAuthenticatedRacePage,
requirePortraitFixture
} = require('./live-site-test-utils');
test('loads a live race page with an authenticated session', async ({ page }) => {
await page.goto(LIVE_SITE_RACE_URL, { waitUntil: 'domcontentloaded' });
await dismissCookieBanner(page);
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
try {
await waitForLoggedInUi(page);
} catch (error) {
await performLiveLogin(page);
await page.goto(LIVE_SITE_RACE_URL, { waitUntil: 'domcontentloaded' });
await dismissCookieBanner(page);
await waitForLoggedInUi(page);
}
async function openLiveFaceAi(page) {
const consoleErrors = [];
await expectRacePageLoaded(page);
page.on('console', (message) => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
}
});
await ensureLiveAuthenticatedRacePage(page);
await expect(page.locator('h1')).toContainText(/HALF MARATHON FIRENZE|Competitions|Gare/i);
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('raceYear')).toBeTruthy();
expect(parsedLaunchUrl.searchParams.get('raceMonthFolder')).toBeTruthy();
expect(parsedLaunchUrl.searchParams.get('raceFolder')).toBeTruthy();
expect(parsedLaunchUrl.searchParams.get('raceStorageRelativeDir')).toBeTruthy();
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();
return {
consoleErrors,
launchUrl: parsedLaunchUrl
};
}
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');
@ -30,4 +61,61 @@ test('loads a live race page with an authenticated session', async ({ page }) =>
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 } = await openLiveFaceAi(page);
expect(launchUrl.searchParams.get('raceStorageRelativeDir')).toContain('/');
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);
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');
expect(consoleErrors.find((entry) => entry.includes('MISSING_RACE_STORAGE')) || '').toBe('');
expect(consoleErrors.find((entry) => entry.includes('[FaceAI] Invalid race data:')) || '').toBe('');
});
test.skip(!LIVE_SITE_RUN_UPLOAD_FLOW, 'Set LIVE_SITE_RUN_UPLOAD_FLOW=1 to exercise the live upload flow.');
test('accepts the supplied portrait image for the live upload flow', async ({ page }) => {
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('test_portrait_1.png');
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();
expect(searchPayload.id || searchPayload.searchId, 'Expected the search creation response to include a search identifier.').toBeTruthy();
await expect(page.locator('.faceai-feedback')).not.toContainText(/Impossibile|Unable|Errore|Error/i);
});

View file

@ -2,11 +2,15 @@ const fs = require('fs');
const path = require('path');
const { expect } = require('@playwright/test');
const WORKSPACE_ROOT = path.resolve(__dirname, '..', '..', '..');
const LIVE_SITE_BASE_URL = process.env.LIVE_SITE_BASE_URL || 'https://www.regalamiunsorriso.it';
const LIVE_SITE_LOGIN_URL = process.env.LIVE_SITE_LOGIN_URL || `${LIVE_SITE_BASE_URL}/login_clienti-it.html`;
const LIVE_SITE_RACE_URL = process.env.LIVE_SITE_RACE_URL || `${LIVE_SITE_BASE_URL}/42%20HALF%20MARATHON%20FIRENZE_gara-1018545---96-1.html`;
const LIVE_FACEAI_BASE_URL = process.env.LIVE_FACEAI_BASE_URL || 'https://ai.regalamiunsorriso.it';
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 AUTH_FILE = path.join(__dirname, '.auth', 'user.json');
function ensureAuthDirectory() {
@ -75,18 +79,45 @@ async function expectRacePageLoaded(page) {
await expect(page.locator('script[src*="_js/rus-ecom-240621.js"]')).toHaveCount(1);
}
async function ensureLiveAuthenticatedRacePage(page) {
await page.goto(LIVE_SITE_RACE_URL, { waitUntil: 'domcontentloaded' });
await dismissCookieBanner(page);
try {
await waitForLoggedInUi(page);
} catch (error) {
await performLiveLogin(page);
await page.goto(LIVE_SITE_RACE_URL, { waitUntil: 'domcontentloaded' });
await dismissCookieBanner(page);
await waitForLoggedInUi(page);
}
await expectRacePageLoaded(page);
}
function requirePortraitFixture() {
if (!fs.existsSync(LIVE_SITE_PORTRAIT_PATH)) {
throw new Error(`LIVE_SITE_PORTRAIT_PATH does not exist: ${LIVE_SITE_PORTRAIT_PATH}`);
}
}
module.exports = {
AUTH_FILE,
LIVE_FACEAI_BASE_URL,
LIVE_SITE_BASE_URL,
LIVE_SITE_LOGIN_URL,
LIVE_SITE_PASSWORD,
LIVE_SITE_PORTRAIT_PATH,
LIVE_SITE_RACE_URL,
LIVE_SITE_RUN_UPLOAD_FLOW,
LIVE_SITE_USERNAME,
dismissCookieBanner,
ensureLiveAuthenticatedRacePage,
ensureAuthDirectory,
expectRacePageLoaded,
loginSubmitLocator,
performLiveLogin,
requirePortraitFixture,
requireCredentials,
waitForLoggedInUi
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View file

@ -258,6 +258,7 @@ function buildFaceAiLaunchUrl() {
var raceYear = getFaceAiStorageValue("faceAiRaceYear", "year");
var raceMonthFolder = getFaceAiStorageValue("faceAiRaceMonthFolder", "monthFolder");
var raceFolder = getFaceAiStorageValue("faceAiRaceFolder", "raceFolder");
var raceStorageRelativeDir = $("#faceAiRaceStorageRelativeDir").val() || [raceYear, raceMonthFolder, raceFolder].filter(Boolean).join("/");
var lang = getCurrentLangValue();
var handoffUrl = (window.faceAiSimulator && window.faceAiSimulator.handoffUrl) || "faceai_handoff.php";
var returnUrl = (window.faceAiSimulator && window.faceAiSimulator.returnUrl) || window.location.href;
@ -268,6 +269,7 @@ function buildFaceAiLaunchUrl() {
"raceYear=" + encodeURIComponent(raceYear),
"raceMonthFolder=" + encodeURIComponent(raceMonthFolder),
"raceFolder=" + encodeURIComponent(raceFolder),
"raceStorageRelativeDir=" + encodeURIComponent(raceStorageRelativeDir),
"lang=" + encodeURIComponent(lang),
"returnUrl=" + encodeURIComponent(returnUrl)
];

View file

@ -11,6 +11,7 @@ try {
$raceYear = faceai_request_value('raceYear');
$raceMonthFolder = faceai_request_value('raceMonthFolder');
$raceFolder = faceai_request_value('raceFolder');
$raceStorageRelativeDir = faceai_request_value('raceStorageRelativeDir');
$lang = faceai_request_value('lang', 'it');
$returnUrl = faceai_request_value('returnUrl');
@ -45,11 +46,12 @@ try {
'name' => $raceName !== '' ? $raceName : $raceId
);
if ($raceYear !== '' && $raceMonthFolder !== '' && $raceFolder !== '') {
if ($raceYear !== '' || $raceMonthFolder !== '' || $raceFolder !== '' || $raceStorageRelativeDir !== '') {
$racePayload['storage'] = array(
'year' => $raceYear,
'monthFolder' => $raceMonthFolder,
'raceFolder' => strtoupper(trim($raceFolder))
'raceFolder' => strtoupper(trim($raceFolder)),
'relativeDir' => $raceStorageRelativeDir
);
}