const { test, expect } = require('@playwright/test'); const { ensureLocalAuthenticatedRacePage, EXPECTED_MATCH_COUNT, FACEAI_BASE_URL, LEGACY_BASE_URL, LEGACY_RACE_ID, SELFIE_NAME, buildHandoffUrl, buildSimulatorUrl, getSearchArtifacts, getSelfiePath, readAuditArtifacts, 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 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; const FACEAI_CONSENT_HEADING_RE = /Prima di continuare|Before you continue/i; const FACEAI_UPLOAD_HEADING_RE = /Carica il tuo selfie|Upload your selfie/i; const LEGACY_BASE_URL_RE = new RegExp(`^${escapeRegExp(LEGACY_BASE_URL)}`); const LEGACY_HOME_URL_RE = new RegExp(`^${escapeRegExp(`${LEGACY_BASE_URL}/index.jsp`)}$`); function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function buildLegacySimulatorReturnMatcher(raceId) { return new RegExp(`^${escapeRegExp(`${LEGACY_BASE_URL}/Foto2.abl?id_gara=${raceId}`)}.*`); } 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_HOME_URL_RE.test(url.toString()), { timeout: SHORT_UI_TIMEOUT_MS }); const consentHeading = page.getByRole('heading', { name: FACEAI_CONSENT_HEADING_RE }); if (await consentHeading.isVisible().catch(() => false)) { await page.getByRole('checkbox', { name: /Confermo di aver letto l’informativa sul trattamento dei dati biometrici\.|I confirm that I have read the biometric data processing notice\./i }).check(); await page.getByRole('button', { name: /Accetto e continuo|I agree and continue/i }).click(); } await expect(page.getByRole('heading', { name: FACEAI_UPLOAD_HEADING_RE })).toBeVisible(); await expect(page.getByRole('button', { name: /Scegli immagine|Choose image/i })).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); } async function readLaunchUrlFromLegacyPage(page) { const launchUrl = await page.evaluate(() => { return typeof buildFaceAiLaunchUrl === 'function' ? buildFaceAiLaunchUrl() : ''; }); expect(launchUrl, 'Expected the legacy race page to expose a FaceAI handoff URL builder.').toBeTruthy(); return new URL(launchUrl, LEGACY_BASE_URL); } async function startSearch(page, selfieName) { 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)); 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)}`); } async function waitForLegacyResult(page, raceId, expectedMatchCount = null) { await page.waitForURL(buildLegacySimulatorReturnMatcher(raceId), { timeout: LEGACY_RETURN_TIMEOUT_MS, waitUntil: 'commit' }); await expect.poll(() => page.url(), { timeout: 15 * 1000, message: 'Expected the legacy race return URL to include FaceAI direct-return parameters.' }).toMatch(/faceaiResultId=|faceaiMatchStorageKey=|faceaiPhotoIds=/); if (expectedMatchCount === null) { return; } await expect(page.locator('#faceAiPhotoCountValue')).toHaveText(String(expectedMatchCount), { timeout: SHORT_UI_TIMEOUT_MS }); } 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 verifyAuditLog(searchId, { expectedMatchCount, expectedRaceId, expectedSelfieName, expectedUserId }) { const artifacts = getSearchArtifacts(searchId); const audit = readAuditArtifacts(searchId); expect(audit.searchRow, `Expected ${artifacts.auditDbPath} to contain an audit row for ${searchId}`).toBeTruthy(); expect(audit.searchRow.search_id).toBe(searchId); expect(audit.searchRow.status).toBe('completed'); expect(audit.searchRow.match_count).toBe(expectedMatchCount); expect(audit.searchRow.race_id).toBe(expectedRaceId); expect(audit.searchRow.user_id).toBe(expectedUserId); expect(audit.searchRow.selfie_name).toBe(expectedSelfieName); expect(audit.searchRow.selfie_sha256).toMatch(/^[a-f0-9]{64}$/i); expect(audit.searchRow.selfie_size_bytes).toBeGreaterThan(0); expect(audit.searchRow.result_id).toBeTruthy(); expect(audit.searchRow.requested_at).toBeGreaterThan(0); expect(audit.searchRow.completed_at).toBeGreaterThan(0); expect(audit.searchRow.redirect_issued_at).toBeGreaterThan(0); expect(audit.searchRow.matches).toHaveLength(expectedMatchCount); const eventTypes = audit.events.map((event) => event.event_type); expect(eventTypes).toContain('search_requested'); expect(eventTypes).toContain('search_completed'); expect(eventTypes).toContain('search_redirect_issued'); const requestedEvent = audit.events.find((event) => event.event_type === 'search_requested'); expect(requestedEvent?.selfie_sha256).toBe(audit.searchRow.selfie_sha256); const fingerprintMatch = audit.fingerprintMatches.find((entry) => entry.search_id === searchId); expect(fingerprintMatch, 'Expected fingerprint lookup to find the original search row').toBeTruthy(); expect(fingerprintMatch.match_count).toBe(expectedMatchCount); expect(fingerprintMatch.result_id).toBe(audit.searchRow.result_id); expect(fingerprintMatch.status).toBe('completed'); return audit; } async function verifyNoResultsAuditLog(searchId, { expectedRaceId, expectedSelfieName, expectedUserId }) { const artifacts = getSearchArtifacts(searchId); const audit = readAuditArtifacts(searchId); expect(audit.searchRow, `Expected ${artifacts.auditDbPath} to contain an audit row for ${searchId}`).toBeTruthy(); expect(audit.searchRow.search_id).toBe(searchId); expect(audit.searchRow.status).toBe('completed'); expect(audit.searchRow.completion_code).toBe('NO_FACES_FOUND'); expect(audit.searchRow.match_count).toBe(0); expect(audit.searchRow.race_id).toBe(expectedRaceId); expect(audit.searchRow.user_id).toBe(expectedUserId); expect(audit.searchRow.selfie_name).toBe(expectedSelfieName); expect(audit.searchRow.selfie_sha256).toMatch(/^[a-f0-9]{64}$/i); expect(audit.searchRow.selfie_size_bytes).toBeGreaterThan(0); expect(audit.searchRow.result_id).toBeTruthy(); expect(audit.searchRow.requested_at).toBeGreaterThan(0); expect(audit.searchRow.completed_at).toBeGreaterThan(0); expect(audit.searchRow.redirect_issued_at).toBeNull(); expect(audit.searchRow.matches).toHaveLength(0); const eventTypes = audit.events.map((event) => event.event_type); expect(eventTypes).toContain('search_requested'); expect(eventTypes).toContain('search_completed'); expect(eventTypes).not.toContain('search_redirect_issued'); const requestedEvent = audit.events.find((event) => event.event_type === 'search_requested'); expect(requestedEvent?.selfie_sha256).toBe(audit.searchRow.selfie_sha256); const fingerprintMatch = audit.fingerprintMatches.find((entry) => entry.search_id === searchId); expect(fingerprintMatch, 'Expected fingerprint lookup to find the original search row').toBeTruthy(); expect(fingerprintMatch.match_count).toBe(0); expect(fingerprintMatch.result_id).toBe(audit.searchRow.result_id); expect(fingerprintMatch.status).toBe('completed'); return audit; } async function closeContexts(contexts) { 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; } } })); } test('runs the legacy Tomcat flow through FaceAI and returns to the filtered legacy result', async ({ page }) => { test.slow(); await launchFromSimulator(page, { raceId: LEGACY_RACE_ID, raceSlug: 'isolotto', raceName: 'Festa sociale UP Isolotto', raceFolder: 'ISOLOTTO' }); const search = await startSearch(page, SELFIE_NAME); await waitForLegacyResult(page, LEGACY_RACE_ID, EXPECTED_MATCH_COUNT); await verifySearchLogs(search.id, { expectedMatchCount: EXPECTED_MATCH_COUNT, expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }); await verifyAuditLog(search.id, { expectedMatchCount: EXPECTED_MATCH_COUNT, expectedRaceId: LEGACY_RACE_ID, expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop(), expectedUserId: 'legacy-user-1' }); }); test('records structured logs for a completed no-results FaceAI search in the dev compose stack', async ({ page }) => { test.slow(); await launchFromSimulator(page, { raceId: LEGACY_RACE_ID, raceSlug: 'isolotto', raceName: 'Festa sociale UP Isolotto', raceFolder: 'ISOLOTTO' }); const search = await startSearch(page, SELFIE_NAME); await waitForSearchCondition(page, search.id, (payload) => { return payload.status === 'completed' && payload.completionCode === 'NO_FACES_FOUND'; }, SEARCH_COMPLETION_TIMEOUT_MS); await verifySearchLogs(search.id, { expectedMatchCount: 0, expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }); await verifyNoResultsAuditLog(search.id, { expectedRaceId: LEGACY_RACE_ID, expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop(), expectedUserId: 'legacy-user-1' }); }); test('builds the legacy FaceAI handoff URL with the exact local race storage metadata', async ({ page }) => { await page.goto(buildSimulatorUrl({ raceId: LEGACY_RACE_ID, raceSlug: 'isolotto', raceName: 'Festa sociale UP Isolotto', raceYear: '2026', raceMonthFolder: '04.APRILE', raceFolder: 'ISOLOTTO' }), { waitUntil: 'domcontentloaded' }); await expect(page.locator('#faceAiRaceYear')).toHaveValue('2026'); await expect(page.locator('#faceAiRaceMonthFolder')).toHaveValue('04.APRILE'); await expect(page.locator('#faceAiRaceFolder')).toHaveValue('ISOLOTTO'); const launchUrl = await readLaunchUrlFromLegacyPage(page); expect(launchUrl.searchParams.get('raceYear')).toBe('2026'); expect(launchUrl.searchParams.get('raceMonthFolder')).toBe('04.APRILE'); expect(launchUrl.searchParams.get('raceFolder')).toBe('ISOLOTTO'); expect(launchUrl.searchParams.get('raceStorageRelativeDir')).toBe('2026/04.APRILE/ISOLOTTO'); }); 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', 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(); 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); await page.getByRole('button', { name: 'Torna alla pagina gara' }).click(); await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('404')); }); 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') { consoleErrors.push(message.text()); } }); 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(handoffUrl.toString(), { waitUntil: 'domcontentloaded' }); await page.waitForURL(FACEAI_HOME_URL_RE, { timeout: SHORT_UI_TIMEOUT_MS }); 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); 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('MISSING_RACE_STORAGE'); await expect.poll(() => { return consoleErrors.find((entry) => entry.includes('[FaceAI] Invalid race data:')) || null; }).toContain('MISSING_RACE_STORAGE'); await page.getByRole('button', { name: 'Back to the race page' }).click(); 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.'); }); 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); }); test('shows the no-face message and allows the user to return to the race page', async ({ page }) => { test.slow(); 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'; }, SEARCH_COMPLETION_TIMEOUT_MS); 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' }); await page.getByRole('button', { name: 'Torna alla pagina gara' }).click(); await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('202')); }); 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; await fileChooser.setFiles(getSelfiePath(SELFIE_NAME)); await expect(page.getByText(SELFIE_NAME.split(/[\\/]+/u).pop())).toBeVisible(); await expect(page.getByRole('button', { name: 'Avvia ricerca Face ID' })).toBeEnabled(); }); test('lets the user retry with a valid photo after a no-face upload and then returns to the filtered legacy result', async ({ page }) => { test.slow(); 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'; }, SEARCH_COMPLETION_TIMEOUT_MS); await expect(page.locator('.faceai-feedback')).toContainText('Nessun volto rilevato nella foto caricata'); await expect(page.locator('input[type="file"]')).toBeEnabled(); const retrySearch = await startSearch(page, SELFIE_NAME); await waitForLegacyResult(page, '202', EXPECTED_MATCH_COUNT); await verifySearchLogs(noFaceSearch.id, { expectedMatchCount: 0, expectedSelfieName: 'DSC_1994.JPG' }); await verifySearchLogs(retrySearch.id, { expectedMatchCount: EXPECTED_MATCH_COUNT, expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }); }); 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' }); await page.waitForURL(LEGACY_HOME_URL_RE, { timeout: SHORT_UI_TIMEOUT_MS }); } finally { await context.close(); } }); test('allows two users to process different photos at the same time', async ({ browser }) => { test.slow(); test.setTimeout(LONG_TEST_TIMEOUT_MS); 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([ startSearch(pages[0], SELFIE_NAME), startSearch(pages[1], 'DSC_1987.JPG') ]); await Promise.all([ waitForLegacyResult(pages[0], '202'), waitForLegacyResult(pages[1], '202') ]); await Promise.all([ verifySearchLogs(searchOne.id, { expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }), 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 }) => { test.slow(); test.setTimeout(LONG_TEST_TIMEOUT_MS); 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([ startSearch(pages[0], SELFIE_NAME), 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'; }, SEARCH_COMPLETION_TIMEOUT_MS); await Promise.all(pages.map((page) => waitForLegacyResult(page, '202'))); await Promise.all([ verifySearchLogs(searchOne.id, { expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }), verifySearchLogs(searchTwo.id, { expectedSelfieName: 'DSC_1987.JPG' }), verifySearchLogs(searchThree.id, { expectedSelfieName: 'DSC_2058.JPG' }) ]); } finally { await closeContexts(contexts); } });