2026-04-12 19:31:12 +02:00
const { test , expect } = require ( '@playwright/test' ) ;
const {
2026-04-22 22:45:44 +02:00
ensureLocalAuthenticatedRacePage ,
2026-04-12 19:31:12 +02:00
EXPECTED _MATCH _COUNT ,
FACEAI _BASE _URL ,
2026-04-22 18:41:37 +02:00
LEGACY _RACE _ID ,
2026-04-22 22:45:44 +02:00
SELFIE _NAME ,
2026-04-12 19:31:12 +02:00
buildHandoffUrl ,
buildSimulatorUrl ,
getSearchArtifacts ,
getSelfiePath ,
readUtf8
} = require ( './faceai-test-utils' ) ;
const FACEAI _HOME _URL _RE = /http:\/\/(localhost|127\.0\.0\.1):3001\/?(?:\?.*)?$/ ;
const FACEAI _CALLBACK _URL _RE = /http:\/\/(localhost|127\.0\.0\.1):3001\/auth\/callback\?token=/ ;
const LEGACY _HOME _URL _RE = /http:\/\/(localhost|127\.0\.0\.1):8080\/index\.jsp$/ ;
2026-04-19 14:18:00 +02:00
const LONG _TEST _TIMEOUT _MS = 3 * 60 * 1000 ;
const SHORT _UI _TIMEOUT _MS = 30 * 1000 ;
const SEARCH _COMPLETION _TIMEOUT _MS = 75 * 1000 ;
const LEGACY _RETURN _TIMEOUT _MS = 75 * 1000 ;
const FILE _CHOOSER _TIMEOUT _MS = 8 * 1000 ;
2026-04-12 19:31:12 +02:00
function buildLegacySimulatorReturnMatcher ( raceId ) {
2026-04-22 18:41:37 +02:00
return new RegExp ( ` http://(localhost|127 \\ .0 \\ .0 \\ .1):8080/Foto2 \\ .abl \\ ?id_gara= ${ raceId } .* ` ) ;
2026-04-12 19:31:12 +02:00
}
function assertLogDoesNotContain ( content , patterns , label ) {
for ( const pattern of patterns ) {
expect ( content , ` ${ label } should not contain ${ pattern } ` ) . not . toMatch ( pattern ) ;
}
}
async function waitForFaceAiHome ( page ) {
await page . waitForURL ( ( url ) => FACEAI _CALLBACK _URL _RE . test ( url . toString ( ) ) || FACEAI _HOME _URL _RE . test ( url . toString ( ) ) , {
2026-04-19 14:18:00 +02:00
timeout : SHORT _UI _TIMEOUT _MS
2026-04-12 19:31:12 +02:00
} ) ;
await expect ( page . getByRole ( 'heading' , { name : 'Trova le tue foto con un selfie' } ) ) . toBeVisible ( ) ;
}
async function launchFromSimulator ( page , options = { } ) {
const simulatorUrl = buildSimulatorUrl ( options ) ;
await page . goto ( simulatorUrl , { waitUntil : 'domcontentloaded' } ) ;
await expect ( page . locator ( '#faceaiLaunchButton' ) ) . toBeVisible ( ) ;
await expect ( page . locator ( 'select#tipoPuntoFoto' ) ) . toHaveCount ( 0 ) ;
await page . locator ( '#faceaiLaunchButton' ) . click ( ) ;
await waitForFaceAiHome ( page ) ;
return simulatorUrl ;
}
async function enterViaHandoff ( page , options = { } ) {
await page . goto ( buildHandoffUrl ( options ) , { waitUntil : 'domcontentloaded' } ) ;
await waitForFaceAiHome ( page ) ;
}
2026-04-19 16:12:48 +02:00
async function readLaunchUrlFromLegacyPage ( page ) {
const launchUrl = await page . evaluate ( ( ) => {
return typeof buildFaceAiLaunchUrl === 'function' ? buildFaceAiLaunchUrl ( ) : '' ;
} ) ;
2026-04-22 18:41:37 +02:00
expect ( launchUrl , 'Expected the legacy race page to expose a FaceAI handoff URL builder.' ) . toBeTruthy ( ) ;
2026-04-19 16:12:48 +02:00
return new URL ( launchUrl , 'http://127.0.0.1:8080' ) ;
}
2026-04-12 19:31:12 +02:00
async function startSearch ( page , selfieName ) {
2026-04-22 22:45:44 +02:00
const selfieLabel = selfieName . split ( /[\\/]+/u ) . pop ( ) ;
2026-04-12 19:31:12 +02:00
const createResponsePromise = page . waitForResponse ( ( response ) => {
return response . url ( ) . includes ( '/api/searches' )
&& response . request ( ) . method ( ) === 'POST'
&& response . status ( ) === 201 ;
} ) ;
await page . locator ( 'input[type="file"]' ) . setInputFiles ( getSelfiePath ( selfieName ) ) ;
2026-04-22 22:45:44 +02:00
await expect ( page . getByText ( selfieLabel ) ) . toBeVisible ( ) ;
2026-04-12 19:31:12 +02:00
await page . getByRole ( 'button' , { name : 'Avvia ricerca Face ID' } ) . click ( ) ;
const createResponse = await createResponsePromise ;
return createResponse . json ( ) ;
}
async function fetchSearchStatus ( page , searchId ) {
return page . evaluate ( async ( { searchId } ) => {
const response = await fetch ( ` /api/searches/ ${ searchId } ` , { credentials : 'include' } ) ;
const body = await response . json ( ) . catch ( ( ) => null ) ;
return {
statusCode : response . status ,
body
} ;
} , { searchId } ) ;
}
async function waitForSearchCondition ( page , searchId , predicate , timeoutMs = 30 * 1000 ) {
const deadline = Date . now ( ) + timeoutMs ;
let lastPayload = null ;
while ( Date . now ( ) < deadline ) {
const payload = await fetchSearchStatus ( page , searchId ) ;
lastPayload = payload ;
if ( payload . statusCode === 200 && predicate ( payload . body ) ) {
return payload . body ;
}
await page . waitForTimeout ( 250 ) ;
}
throw new Error ( ` Timed out waiting for search ${ searchId } . Last payload: ${ JSON . stringify ( lastPayload ) } ` ) ;
}
2026-04-19 14:18:00 +02:00
async function waitForLegacyResult ( page , raceId , expectedMatchCount = null ) {
await page . waitForURL ( buildLegacySimulatorReturnMatcher ( raceId ) , {
timeout : LEGACY _RETURN _TIMEOUT _MS ,
waitUntil : 'commit'
2026-04-12 19:31:12 +02:00
} ) ;
2026-04-19 14:18:00 +02:00
await expect . poll ( ( ) => page . url ( ) , {
timeout : 15 * 1000 ,
2026-04-22 18:41:37 +02:00
message : 'Expected the legacy race return URL to include FaceAI direct-return parameters.'
} ) . toMatch ( /faceaiResultId=|faceaiMatchStorageKey=|faceaiPhotoIds=/ ) ;
2026-04-19 14:18:00 +02:00
2026-04-12 19:31:12 +02:00
if ( expectedMatchCount === null ) {
return ;
}
2026-04-22 18:41:37 +02:00
await expect ( page . locator ( '#faceAiPhotoCountValue' ) ) . toHaveText ( String ( expectedMatchCount ) , {
timeout : SHORT _UI _TIMEOUT _MS
} ) ;
2026-04-12 19:31:12 +02:00
}
async function verifySearchLogs ( searchId , { expectedMatchCount , expectedSelfieName } ) {
const artifacts = getSearchArtifacts ( searchId ) ;
const [ backendLog , processorLog , workerLog , matcherLog ] = await Promise . all ( [
readUtf8 ( artifacts . backendLogPath ) ,
readUtf8 ( artifacts . processorLogPath ) ,
readUtf8 ( artifacts . workerLogPath ) ,
readUtf8 ( artifacts . matcherLogPath )
] ) ;
expect ( workerLog ) . toContain ( 'Completed FaceAI search' ) ;
if ( expectedMatchCount !== undefined ) {
expect ( workerLog ) . toContain ( ` "matchCount": ${ expectedMatchCount } ` ) ;
}
if ( expectedSelfieName ) {
expect ( matcherLog ) . toContain ( expectedSelfieName ) ;
}
assertLogDoesNotContain ( backendLog , [ /\bnpm error\b/i , /\berror:\b/i , /\bfailed\b/i ] , 'backend.log' ) ;
assertLogDoesNotContain ( processorLog , [ new RegExp ( ` Failed FaceAI search ${ searchId } ` , 'i' ) , /\bnpm error\b/i ] , 'processor.log' ) ;
assertLogDoesNotContain ( workerLog , [ /FaceAI search failed/i ] , 'worker.log' ) ;
assertLogDoesNotContain ( matcherLog , [ /\[ERROR\]/i , /Traceback/i ] , 'matcher.log' ) ;
return { backendLog , processorLog , workerLog , matcherLog } ;
}
async function closeContexts ( contexts ) {
2026-04-19 14:18:00 +02:00
await Promise . all ( contexts . map ( async ( context ) => {
try {
await context . close ( ) ;
} catch ( error ) {
if ( ! /ENOENT|Target page, context or browser has been closed/i . test ( String ( error ) ) ) {
throw error ;
}
}
} ) ) ;
2026-04-12 19:31:12 +02:00
}
2026-04-22 18:41:37 +02:00
test ( 'runs the legacy Tomcat flow through FaceAI and returns to the filtered legacy result' , async ( { page } ) => {
2026-04-19 14:18:00 +02:00
test . slow ( ) ;
2026-04-12 19:31:12 +02:00
await launchFromSimulator ( page , {
2026-04-22 18:41:37 +02:00
raceId : LEGACY _RACE _ID ,
2026-04-22 22:45:44 +02:00
raceSlug : 'isolotto' ,
raceName : 'Festa sociale UP Isolotto' ,
raceFolder : 'ISOLOTTO'
2026-04-12 19:31:12 +02:00
} ) ;
2026-04-22 22:45:44 +02:00
const search = await startSearch ( page , SELFIE _NAME ) ;
2026-04-12 19:31:12 +02:00
2026-04-22 18:41:37 +02:00
await waitForLegacyResult ( page , LEGACY _RACE _ID , EXPECTED _MATCH _COUNT ) ;
2026-04-12 19:31:12 +02:00
await verifySearchLogs ( search . id , {
expectedMatchCount : EXPECTED _MATCH _COUNT ,
2026-04-22 22:45:44 +02:00
expectedSelfieName : SELFIE _NAME . split ( /[\\/]+/u ) . pop ( )
2026-04-12 19:31:12 +02:00
} ) ;
} ) ;
2026-04-22 18:41:37 +02:00
test ( 'builds the legacy FaceAI handoff URL with the exact local race storage metadata' , async ( { page } ) => {
2026-04-19 16:12:48 +02:00
await page . goto ( buildSimulatorUrl ( {
2026-04-22 18:41:37 +02:00
raceId : LEGACY _RACE _ID ,
2026-04-22 22:45:44 +02:00
raceSlug : 'isolotto' ,
raceName : 'Festa sociale UP Isolotto' ,
2026-04-19 16:12:48 +02:00
raceYear : '2026' ,
raceMonthFolder : '04.APRILE' ,
2026-04-22 22:45:44 +02:00
raceFolder : 'ISOLOTTO'
2026-04-19 16:12:48 +02:00
} ) , { waitUntil : 'domcontentloaded' } ) ;
await expect ( page . locator ( '#faceAiRaceYear' ) ) . toHaveValue ( '2026' ) ;
await expect ( page . locator ( '#faceAiRaceMonthFolder' ) ) . toHaveValue ( '04.APRILE' ) ;
2026-04-22 22:45:44 +02:00
await expect ( page . locator ( '#faceAiRaceFolder' ) ) . toHaveValue ( 'ISOLOTTO' ) ;
2026-04-19 16:12:48 +02:00
const launchUrl = await readLaunchUrlFromLegacyPage ( page ) ;
expect ( launchUrl . searchParams . get ( 'raceYear' ) ) . toBe ( '2026' ) ;
expect ( launchUrl . searchParams . get ( 'raceMonthFolder' ) ) . toBe ( '04.APRILE' ) ;
2026-04-22 22:45:44 +02:00
expect ( launchUrl . searchParams . get ( 'raceFolder' ) ) . toBe ( 'ISOLOTTO' ) ;
expect ( launchUrl . searchParams . get ( 'raceStorageRelativeDir' ) ) . toBe ( '2026/04.APRILE/ISOLOTTO' ) ;
2026-04-19 16:12:48 +02:00
} ) ;
2026-04-12 19:31:12 +02:00
test ( 'shows the unsupported-race message when the current race has no PKL data and lets the user go back' , async ( { page } ) => {
2026-04-20 00:11:03 +02:00
const consoleErrors = [ ] ;
page . on ( 'console' , ( message ) => {
if ( message . type ( ) === 'error' ) {
consoleErrors . push ( message . text ( ) ) ;
}
} ) ;
2026-04-12 19:31:12 +02:00
await launchFromSimulator ( page , {
raceId : '404' ,
raceSlug : 'corsa-di-livorno' ,
raceName : 'Corsa di Livorno' ,
raceFolder : 'LIVORNO'
} ) ;
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 ( ) ;
2026-04-20 00:11:03 +02:00
await expect . poll ( ( ) => {
return consoleErrors . find ( ( entry ) => entry . includes ( '[FaceAI] Invalid race data:' ) ) || null ;
} ) . toBeNull ( ) ;
2026-04-12 19:31:12 +02:00
await page . waitForTimeout ( 2000 ) ;
await expect ( page ) . toHaveURL ( FACEAI _HOME _URL _RE ) ;
2026-04-19 12:10:18 +02:00
await page . getByRole ( 'button' , { name : 'Torna alla pagina gara' } ) . click ( ) ;
2026-04-12 19:31:12 +02:00
await expect ( page ) . toHaveURL ( buildLegacySimulatorReturnMatcher ( '404' ) ) ;
} ) ;
2026-04-20 00:11:03 +02:00
test ( 'shows a localized invalid-race error when the handoff omits race storage metadata' , async ( { page } ) => {
2026-04-12 19:31:12 +02:00
const consoleErrors = [ ] ;
page . on ( 'console' , ( message ) => {
if ( message . type ( ) === 'error' ) {
consoleErrors . push ( message . text ( ) ) ;
}
} ) ;
2026-04-20 00:11:03 +02:00
const handoffUrl = new URL ( buildHandoffUrl ( {
2026-04-12 19:31:12 +02:00
raceId : '405' ,
lang : 'en' ,
raceSlug : 'ghost-race' ,
raceName : 'Ghost Race' ,
raceFolder : 'THIS RACE DOES NOT EXIST'
2026-04-20 00:11:03 +02:00
} ) ) ;
handoffUrl . searchParams . delete ( 'raceYear' ) ;
handoffUrl . searchParams . delete ( 'raceMonthFolder' ) ;
handoffUrl . searchParams . delete ( 'raceFolder' ) ;
2026-04-12 19:31:12 +02:00
2026-04-20 00:11:03 +02:00
await page . goto ( handoffUrl . toString ( ) , { waitUntil : 'domcontentloaded' } ) ;
2026-04-12 19:31:12 +02:00
await page . waitForURL ( FACEAI _HOME _URL _RE , {
2026-04-19 14:18:00 +02:00
timeout : SHORT _UI _TIMEOUT _MS
2026-04-12 19:31:12 +02:00
} ) ;
await expect ( page . getByRole ( 'heading' , { name : 'Find your photos with a selfie' } ) ) . toBeVisible ( ) ;
await expect ( page . locator ( '.faceai-feedback' ) ) . toContainText ( 'The race data received for this session is invalid. Go back to the race page and reopen Face ID from the correct race.' ) ;
await expect ( page . locator ( 'input[type="file"]' ) ) . toBeDisabled ( ) ;
await expect ( page . getByRole ( 'button' , { name : 'Choose image' } ) ) . toBeDisabled ( ) ;
await expect ( page . getByRole ( 'button' , { name : 'Start Face ID search' } ) ) . toHaveCount ( 0 ) ;
2026-04-19 12:10:18 +02:00
await expect ( page . getByRole ( 'button' , { name : 'Back to the race page' } ) ) . toBeVisible ( ) ;
2026-04-12 19:31:12 +02:00
await expect . poll ( ( ) => {
return consoleErrors . find ( ( entry ) => entry . includes ( '[FaceAI] Invalid race data:' ) ) || null ;
2026-04-20 00:11:03 +02:00
} ) . toContain ( 'MISSING_RACE_STORAGE' ) ;
2026-04-12 19:31:12 +02:00
await expect . poll ( ( ) => {
return consoleErrors . find ( ( entry ) => entry . includes ( '[FaceAI] Invalid race data:' ) ) || null ;
2026-04-20 00:11:03 +02:00
} ) . toContain ( 'MISSING_RACE_STORAGE' ) ;
2026-04-12 19:31:12 +02:00
2026-04-19 12:10:18 +02:00
await page . getByRole ( 'button' , { name : 'Back to the race page' } ) . click ( ) ;
2026-04-12 19:31:12 +02:00
await expect ( page ) . toHaveURL ( buildLegacySimulatorReturnMatcher ( '405' ) ) ;
} ) ;
test ( 'rejects a not-logged-in user after clicking the Face ID button and sends them back to the original race page' , async ( { page } ) => {
const simulatorUrl = buildSimulatorUrl ( {
raceId : '202' ,
raceSlug : 'mezza-di-pisa' ,
raceName : 'Mezza di Pisa' ,
raceFolder : 'PISA'
} ) ;
await page . goto ( simulatorUrl , { waitUntil : 'domcontentloaded' } ) ;
await expect ( page . locator ( '#faceaiLaunchButton' ) ) . toBeVisible ( ) ;
await page . evaluate ( ( ) => {
if ( window . faceAiSimulator ) {
delete window . faceAiSimulator . devUserId ;
delete window . faceAiSimulator . devDisplayName ;
delete window . faceAiSimulator . devEmail ;
delete window . faceAiSimulator . devMembershipStatus ;
}
} ) ;
await page . locator ( '#faceaiLaunchButton' ) . click ( ) ;
await expect ( page ) . toHaveURL ( buildLegacySimulatorReturnMatcher ( '202' ) ) ;
await expect ( page ) . not . toHaveURL ( FACEAI _HOME _URL _RE ) ;
await expect ( page . locator ( '#faceAiErrorModal' ) ) . toBeVisible ( ) ;
await expect ( page . locator ( '#faceAiErrorModalLabel' ) ) . toContainText ( 'Face ID non disponibile' ) ;
await expect ( page . locator ( '#faceAiErrorModalMessage' ) ) . toContainText ( 'Il servizio Face ID non e al momento disponibile. Riprova piu tardi.' ) ;
} ) ;
2026-04-22 22:45:44 +02:00
test ( 'authenticates with the seeded local user and lets that user browse and launch the Livorno race page' , async ( { page } ) => {
await ensureLocalAuthenticatedRacePage ( page , {
raceId : '1018557' ,
raceName : 'VIVICITTA LIVORNO' ,
raceYear : '2026' ,
raceMonthFolder : '04.APRILE' ,
raceFolder : 'LIVORNO'
} ) ;
await page . locator ( '#faceaiLaunchButton' ) . click ( ) ;
await waitForFaceAiHome ( page ) ;
} ) ;
2026-04-12 19:31:12 +02:00
test ( 'shows the no-face message and allows the user to return to the race page' , async ( { page } ) => {
2026-04-19 14:18:00 +02:00
test . slow ( ) ;
2026-04-12 19:31:12 +02:00
await launchFromSimulator ( page , {
raceId : '202' ,
raceSlug : 'mezza-di-pisa' ,
raceName : 'Mezza di Pisa' ,
raceFolder : 'PISA'
} ) ;
const search = await startSearch ( page , 'DSC_1994.JPG' ) ;
await waitForSearchCondition ( page , search . id , ( payload ) => {
return payload . status === 'completed' && payload . completionCode === 'NO_FACES_FOUND' ;
2026-04-19 14:18:00 +02:00
} , SEARCH _COMPLETION _TIMEOUT _MS ) ;
2026-04-12 19:31:12 +02:00
await expect ( page . locator ( '.faceai-feedback' ) ) . toContainText ( 'Nessun volto rilevato nella foto caricata' ) ;
await page . waitForTimeout ( 2000 ) ;
await expect ( page ) . toHaveURL ( FACEAI _HOME _URL _RE ) ;
await verifySearchLogs ( search . id , {
expectedMatchCount : 0 ,
expectedSelfieName : 'DSC_1994.JPG'
} ) ;
2026-04-19 12:10:18 +02:00
await page . getByRole ( 'button' , { name : 'Torna alla pagina gara' } ) . click ( ) ;
2026-04-12 19:31:12 +02:00
await expect ( page ) . toHaveURL ( buildLegacySimulatorReturnMatcher ( '202' ) ) ;
} ) ;
2026-04-19 14:18:00 +02:00
test ( 'opens the file chooser when the user clicks the upload button' , async ( { page } ) => {
await launchFromSimulator ( page , {
raceId : '202' ,
raceSlug : 'mezza-di-pisa' ,
raceName : 'Mezza di Pisa' ,
raceFolder : 'PISA'
} ) ;
const fileChooserPromise = page . waitForEvent ( 'filechooser' , { timeout : FILE _CHOOSER _TIMEOUT _MS } ) ;
await page . getByRole ( 'button' , { name : 'Scegli immagine' } ) . click ( ) ;
const fileChooser = await fileChooserPromise ;
2026-04-22 22:45:44 +02:00
await fileChooser . setFiles ( getSelfiePath ( SELFIE _NAME ) ) ;
await expect ( page . getByText ( SELFIE _NAME . split ( /[\\/]+/u ) . pop ( ) ) ) . toBeVisible ( ) ;
2026-04-19 14:18:00 +02:00
await expect ( page . getByRole ( 'button' , { name : 'Avvia ricerca Face ID' } ) ) . toBeEnabled ( ) ;
} ) ;
2026-04-12 19:31:12 +02:00
test ( 'lets the user retry with a valid photo after a no-face upload and then returns to the filtered legacy result' , async ( { page } ) => {
2026-04-19 14:18:00 +02:00
test . slow ( ) ;
2026-04-12 19:31:12 +02:00
await launchFromSimulator ( page , {
raceId : '202' ,
raceSlug : 'mezza-di-pisa' ,
raceName : 'Mezza di Pisa' ,
raceFolder : 'PISA'
} ) ;
const noFaceSearch = await startSearch ( page , 'DSC_1994.JPG' ) ;
await waitForSearchCondition ( page , noFaceSearch . id , ( payload ) => {
return payload . status === 'completed' && payload . completionCode === 'NO_FACES_FOUND' ;
2026-04-19 14:18:00 +02:00
} , SEARCH _COMPLETION _TIMEOUT _MS ) ;
2026-04-12 19:31:12 +02:00
await expect ( page . locator ( '.faceai-feedback' ) ) . toContainText ( 'Nessun volto rilevato nella foto caricata' ) ;
await expect ( page . locator ( 'input[type="file"]' ) ) . toBeEnabled ( ) ;
2026-04-22 22:45:44 +02:00
const retrySearch = await startSearch ( page , SELFIE _NAME ) ;
2026-04-19 14:18:00 +02:00
await waitForLegacyResult ( page , '202' , EXPECTED _MATCH _COUNT ) ;
2026-04-12 19:31:12 +02:00
await verifySearchLogs ( noFaceSearch . id , {
expectedMatchCount : 0 ,
expectedSelfieName : 'DSC_1994.JPG'
} ) ;
await verifySearchLogs ( retrySearch . id , {
expectedMatchCount : EXPECTED _MATCH _COUNT ,
2026-04-22 22:45:44 +02:00
expectedSelfieName : SELFIE _NAME . split ( /[\\/]+/u ) . pop ( )
2026-04-12 19:31:12 +02:00
} ) ;
} ) ;
test ( 'redirects direct-entry users without FaceAI session data back to the legacy site' , async ( { browser } ) => {
const context = await browser . newContext ( ) ;
const page = await context . newPage ( ) ;
try {
await page . goto ( ` ${ FACEAI _BASE _URL } / ` , { waitUntil : 'domcontentloaded' } ) ;
2026-04-19 14:18:00 +02:00
await page . waitForURL ( LEGACY _HOME _URL _RE , { timeout : SHORT _UI _TIMEOUT _MS } ) ;
2026-04-12 19:31:12 +02:00
} finally {
await context . close ( ) ;
}
} ) ;
test ( 'allows two users to process different photos at the same time' , async ( { browser } ) => {
2026-04-19 14:18:00 +02:00
test . slow ( ) ;
test . setTimeout ( LONG _TEST _TIMEOUT _MS ) ;
2026-04-12 19:31:12 +02:00
const contexts = [ await browser . newContext ( ) , await browser . newContext ( ) ] ;
const pages = await Promise . all ( contexts . map ( ( context ) => context . newPage ( ) ) ) ;
try {
await Promise . all ( [
enterViaHandoff ( pages [ 0 ] , { userId : 'concurrency-user-1' } ) ,
enterViaHandoff ( pages [ 1 ] , { userId : 'concurrency-user-2' } )
] ) ;
const [ searchOne , searchTwo ] = await Promise . all ( [
2026-04-22 22:45:44 +02:00
startSearch ( pages [ 0 ] , SELFIE _NAME ) ,
2026-04-12 19:31:12 +02:00
startSearch ( pages [ 1 ] , 'DSC_1987.JPG' )
] ) ;
await Promise . all ( [
2026-04-19 14:18:00 +02:00
waitForLegacyResult ( pages [ 0 ] , '202' ) ,
waitForLegacyResult ( pages [ 1 ] , '202' )
2026-04-12 19:31:12 +02:00
] ) ;
await Promise . all ( [
2026-04-22 22:45:44 +02:00
verifySearchLogs ( searchOne . id , { expectedSelfieName : SELFIE _NAME . split ( /[\\/]+/u ) . pop ( ) } ) ,
2026-04-12 19:31:12 +02:00
verifySearchLogs ( searchTwo . id , { expectedSelfieName : 'DSC_1987.JPG' } )
] ) ;
} finally {
await closeContexts ( contexts ) ;
}
} ) ;
test ( 'queues the third user until a worker is free and then completes all three searches normally' , async ( { browser } ) => {
2026-04-19 14:18:00 +02:00
test . slow ( ) ;
test . setTimeout ( LONG _TEST _TIMEOUT _MS ) ;
2026-04-12 19:31:12 +02:00
const contexts = [ await browser . newContext ( ) , await browser . newContext ( ) , await browser . newContext ( ) ] ;
const pages = await Promise . all ( contexts . map ( ( context ) => context . newPage ( ) ) ) ;
try {
await Promise . all ( [
enterViaHandoff ( pages [ 0 ] , { userId : 'queue-user-1' } ) ,
enterViaHandoff ( pages [ 1 ] , { userId : 'queue-user-2' } ) ,
enterViaHandoff ( pages [ 2 ] , { userId : 'queue-user-3' } )
] ) ;
const [ searchOne , searchTwo , searchThree ] = await Promise . all ( [
2026-04-22 22:45:44 +02:00
startSearch ( pages [ 0 ] , SELFIE _NAME ) ,
2026-04-12 19:31:12 +02:00
startSearch ( pages [ 1 ] , 'DSC_1987.JPG' ) ,
startSearch ( pages [ 2 ] , 'DSC_2058.JPG' )
] ) ;
const searchSessions = [
{ page : pages [ 0 ] , searchId : searchOne . id } ,
{ page : pages [ 1 ] , searchId : searchTwo . id } ,
{ page : pages [ 2 ] , searchId : searchThree . id }
] ;
let queuedSearch = null ;
const deadline = Date . now ( ) + 30 * 1000 ;
while ( Date . now ( ) < deadline && ! queuedSearch ) {
const statuses = await Promise . all ( searchSessions . map ( async ( session ) => {
const payload = await fetchSearchStatus ( session . page , session . searchId ) ;
return {
... session ,
search : payload . body
} ;
} ) ) ;
const processingCount = statuses . filter ( ( item ) => item . search ? . status === 'processing' ) . length ;
queuedSearch = processingCount >= 2
? statuses . find ( ( item ) => item . search ? . status === 'queued' ) || null
: null ;
if ( ! queuedSearch ) {
await pages [ 0 ] . waitForTimeout ( 250 ) ;
}
}
expect ( queuedSearch , 'one search should remain queued while two worker slots are busy' ) . toBeTruthy ( ) ;
await waitForSearchCondition ( queuedSearch . page , queuedSearch . searchId , ( payload ) => {
return payload . status === 'processing' || payload . status === 'completed' ;
2026-04-19 14:18:00 +02:00
} , SEARCH _COMPLETION _TIMEOUT _MS ) ;
2026-04-12 19:31:12 +02:00
2026-04-19 14:18:00 +02:00
await Promise . all ( pages . map ( ( page ) => waitForLegacyResult ( page , '202' ) ) ) ;
2026-04-12 19:31:12 +02:00
await Promise . all ( [
2026-04-22 22:45:44 +02:00
verifySearchLogs ( searchOne . id , { expectedSelfieName : SELFIE _NAME . split ( /[\\/]+/u ) . pop ( ) } ) ,
2026-04-12 19:31:12 +02:00
verifySearchLogs ( searchTwo . id , { expectedSelfieName : 'DSC_1987.JPG' } ) ,
verifySearchLogs ( searchThree . id , { expectedSelfieName : 'DSC_2058.JPG' } )
] ) ;
} finally {
await closeContexts ( contexts ) ;
}
} ) ;