End to end tests
This commit is contained in:
parent
c71e4b4cd0
commit
fed82d1ae8
26 changed files with 1016 additions and 37 deletions
399
faceai/tests/e2e/faceai-simulator.spec.js
Normal file
399
faceai/tests/e2e/faceai-simulator.spec.js
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
const {
|
||||
EXPECTED_MATCH_COUNT,
|
||||
FACEAI_BASE_URL,
|
||||
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 FACEAI_RETURN_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):8080\/faceai_return\.php\?resultId=.*token=.*/;
|
||||
const LEGACY_HOME_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):8080\/index\.jsp$/;
|
||||
|
||||
function buildLegacySimulatorReturnMatcher(raceId) {
|
||||
return new RegExp(`http:\\/\\/(localhost|127\\.0\\.0\\.1):8080\\/faceai_simulator\\.php\\?raceId=${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_CALLBACK_URL_RE.test(url.toString()) || FACEAI_HOME_URL_RE.test(url.toString()), {
|
||||
timeout: 60 * 1000
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
await expect(page.getByText(selfieName)).toBeVisible();
|
||||
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)}`);
|
||||
}
|
||||
|
||||
async function waitForLegacyResult(page, expectedMatchCount = null) {
|
||||
await page.waitForURL(FACEAI_RETURN_URL_RE, {
|
||||
timeout: 6 * 60 * 1000
|
||||
});
|
||||
await expect(page.locator('.sim-banner')).toContainText('Vista filtrata da FaceAI');
|
||||
if (expectedMatchCount === null) {
|
||||
await expect(page.locator('.gallery-card').first()).toBeVisible();
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(page.locator('.sim-banner')).toContainText(String(expectedMatchCount));
|
||||
await expect(page.locator('.gallery-card')).toHaveCount(expectedMatchCount);
|
||||
}
|
||||
|
||||
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) {
|
||||
await Promise.all(contexts.map((context) => context.close()));
|
||||
}
|
||||
|
||||
test('runs the simulator flow through FaceAI and returns to the filtered legacy result', async ({ page }) => {
|
||||
await launchFromSimulator(page, {
|
||||
raceId: '202',
|
||||
raceSlug: 'mezza-di-pisa',
|
||||
raceName: 'Mezza di Pisa',
|
||||
raceFolder: 'PISA'
|
||||
});
|
||||
|
||||
const search = await startSearch(page, 'DSC_1960.JPG');
|
||||
|
||||
await waitForLegacyResult(page, EXPECTED_MATCH_COUNT);
|
||||
await expect(page.locator('.gallery-card').filter({ hasText: 'DSC_1960.JPG' }).first()).toBeVisible();
|
||||
|
||||
await verifySearchLogs(search.id, {
|
||||
expectedMatchCount: EXPECTED_MATCH_COUNT,
|
||||
expectedSelfieName: 'DSC_1960.JPG'
|
||||
});
|
||||
});
|
||||
|
||||
test('shows the unsupported-race message when the current race has no PKL data and lets the user go back', async ({ page }) => {
|
||||
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 page.waitForTimeout(2000);
|
||||
await expect(page).toHaveURL(FACEAI_HOME_URL_RE);
|
||||
|
||||
await page.getByRole('link', { name: 'Torna alla pagina gara' }).click();
|
||||
await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('404'));
|
||||
});
|
||||
|
||||
test('shows a localized invalid-race error when session race data points to a missing folder', async ({ page }) => {
|
||||
const consoleErrors = [];
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') {
|
||||
consoleErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
|
||||
const simulatorUrl = buildSimulatorUrl({
|
||||
raceId: '405',
|
||||
lang: 'en',
|
||||
raceSlug: 'ghost-race',
|
||||
raceName: 'Ghost Race',
|
||||
raceFolder: 'THIS RACE DOES NOT EXIST'
|
||||
});
|
||||
|
||||
await page.goto(simulatorUrl, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('#faceaiLaunchButton')).toBeVisible();
|
||||
await page.locator('#faceaiLaunchButton').click();
|
||||
|
||||
await page.waitForURL(FACEAI_HOME_URL_RE, {
|
||||
timeout: 60 * 1000
|
||||
});
|
||||
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('link', { 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');
|
||||
await expect.poll(() => {
|
||||
return consoleErrors.find((entry) => entry.includes('[FaceAI] Invalid race data:')) || null;
|
||||
}).toContain('THIS RACE DOES NOT EXIST');
|
||||
|
||||
await page.getByRole('link', { 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('shows the no-face message and allows the user to return to the race page', async ({ page }) => {
|
||||
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';
|
||||
}, 2 * 60 * 1000);
|
||||
|
||||
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('link', { name: 'Torna alla pagina gara' }).click();
|
||||
await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('202'));
|
||||
});
|
||||
|
||||
test('lets the user retry with a valid photo after a no-face upload and then returns to the filtered legacy result', async ({ page }) => {
|
||||
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';
|
||||
}, 2 * 60 * 1000);
|
||||
|
||||
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, 'DSC_1960.JPG');
|
||||
await waitForLegacyResult(page, EXPECTED_MATCH_COUNT);
|
||||
|
||||
await verifySearchLogs(noFaceSearch.id, {
|
||||
expectedMatchCount: 0,
|
||||
expectedSelfieName: 'DSC_1994.JPG'
|
||||
});
|
||||
await verifySearchLogs(retrySearch.id, {
|
||||
expectedMatchCount: EXPECTED_MATCH_COUNT,
|
||||
expectedSelfieName: 'DSC_1960.JPG'
|
||||
});
|
||||
});
|
||||
|
||||
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: 30 * 1000 });
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('allows two users to process different photos at the same time', async ({ browser }) => {
|
||||
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], 'DSC_1960.JPG'),
|
||||
startSearch(pages[1], 'DSC_1987.JPG')
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
waitForLegacyResult(pages[0]),
|
||||
waitForLegacyResult(pages[1])
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
verifySearchLogs(searchOne.id, { expectedSelfieName: 'DSC_1960.JPG' }),
|
||||
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 }) => {
|
||||
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], 'DSC_1960.JPG'),
|
||||
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';
|
||||
}, 2 * 60 * 1000);
|
||||
|
||||
await Promise.all(pages.map((page) => waitForLegacyResult(page)));
|
||||
|
||||
await Promise.all([
|
||||
verifySearchLogs(searchOne.id, { expectedSelfieName: 'DSC_1960.JPG' }),
|
||||
verifySearchLogs(searchTwo.id, { expectedSelfieName: 'DSC_1987.JPG' }),
|
||||
verifySearchLogs(searchThree.id, { expectedSelfieName: 'DSC_2058.JPG' })
|
||||
]);
|
||||
} finally {
|
||||
await closeContexts(contexts);
|
||||
}
|
||||
});
|
||||
203
faceai/tests/e2e/faceai-test-utils.js
Normal file
203
faceai/tests/e2e/faceai-test-utils.js
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const { spawn } = require('node:child_process');
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..', '..');
|
||||
const WORKSPACE_ROOT = path.resolve(ROOT_DIR, '..');
|
||||
const LOG_ROOT = path.join(ROOT_DIR, 'logs');
|
||||
const SEARCH_LOG_ROOT = path.join(LOG_ROOT, 'searches');
|
||||
const FACEAI_BASE_URL = process.env.FACEAI_E2E_BASE_URL || 'http://127.0.0.1:3001';
|
||||
const SIMULATOR_URL = process.env.FACEAI_E2E_SIMULATOR_URL || 'http://127.0.0.1:8080/faceai_simulator.php?raceId=202&lang=it';
|
||||
const LEGACY_BASE_URL = process.env.FACEAI_E2E_LEGACY_BASE_URL || 'http://127.0.0.1:8080';
|
||||
const LEGACY_HOME_URL = process.env.FACEAI_E2E_LEGACY_HOME_URL || `${LEGACY_BASE_URL}/index.jsp`;
|
||||
const SELFIE_NAME = process.env.FACEAI_E2E_SELFIE || 'DSC_1960.JPG';
|
||||
const EXPECTED_MATCH_COUNT = Number(process.env.FACEAI_E2E_EXPECTED_MATCH_COUNT || '6');
|
||||
|
||||
function quoteShellArg(value) {
|
||||
if (!/[\s"]/u.test(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return `"${value.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
function runCommand(command, args, options = {}) {
|
||||
const { cwd = ROOT_DIR, allowFailure = false } = options;
|
||||
const executable = process.platform === 'win32' && command === 'npm' ? 'npm.cmd' : command;
|
||||
const useShell = process.platform === 'win32';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = useShell
|
||||
? spawn([executable, ...args].map(quoteShellArg).join(' '), {
|
||||
cwd,
|
||||
env: process.env,
|
||||
shell: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
: spawn(executable, args, {
|
||||
cwd,
|
||||
env: process.env,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
child.on('error', reject);
|
||||
child.on('close', (code) => {
|
||||
const result = { code, stdout, stderr };
|
||||
if (code === 0 || allowFailure) {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
const error = new Error(`Command failed: ${executable} ${args.join(' ')}`);
|
||||
error.result = result;
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function dockerCompose(args, options) {
|
||||
return runCommand('docker', ['compose', ...args], options);
|
||||
}
|
||||
|
||||
async function prepareHostState() {
|
||||
await fs.rm(LOG_ROOT, { recursive: true, force: true });
|
||||
await fs.mkdir(LOG_ROOT, { recursive: true });
|
||||
}
|
||||
|
||||
async function waitForHttp(url, validate, timeoutMs = 3 * 60 * 1000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastError = null;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const bodyText = await response.text();
|
||||
let parsedBody = null;
|
||||
|
||||
try {
|
||||
parsedBody = JSON.parse(bodyText);
|
||||
} catch {
|
||||
parsedBody = null;
|
||||
}
|
||||
|
||||
if (validate({ response, bodyText, parsedBody })) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastError = new Error(`Readiness check did not pass for ${url}.`);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
throw lastError || new Error(`Timed out waiting for ${url}`);
|
||||
}
|
||||
|
||||
function getSelfiePath(fileName = SELFIE_NAME) {
|
||||
return path.join(WORKSPACE_ROOT, 'test_pkl', 'test_images', fileName);
|
||||
}
|
||||
|
||||
function buildSimulatorUrl({
|
||||
raceId = '202',
|
||||
lang = 'it',
|
||||
raceSlug = 'mezza-di-pisa',
|
||||
raceName = 'Mezza di Pisa',
|
||||
raceYear = '2026',
|
||||
raceMonthFolder = '04.APRILE',
|
||||
raceFolder = 'PISA'
|
||||
} = {}) {
|
||||
const url = new URL('/faceai_simulator.php', LEGACY_BASE_URL);
|
||||
url.searchParams.set('raceId', raceId);
|
||||
url.searchParams.set('lang', lang);
|
||||
url.searchParams.set('raceSlug', raceSlug);
|
||||
url.searchParams.set('raceName', raceName);
|
||||
url.searchParams.set('raceYear', raceYear);
|
||||
url.searchParams.set('raceMonthFolder', raceMonthFolder);
|
||||
url.searchParams.set('raceFolder', raceFolder);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function buildHandoffUrl({
|
||||
raceId = '202',
|
||||
lang = 'it',
|
||||
raceSlug = 'mezza-di-pisa',
|
||||
raceName = 'Mezza di Pisa',
|
||||
raceYear = '2026',
|
||||
raceMonthFolder = '04.APRILE',
|
||||
raceFolder = 'PISA',
|
||||
userId = '1',
|
||||
displayName = `Local Test User ${userId}`,
|
||||
email = `local-test-${userId}@example.invalid`,
|
||||
membershipStatus = 'active',
|
||||
returnUrl = buildSimulatorUrl({ raceId, lang, raceSlug, raceName, raceYear, raceMonthFolder, raceFolder })
|
||||
} = {}) {
|
||||
const url = new URL('/faceai_handoff.php', LEGACY_BASE_URL);
|
||||
url.searchParams.set('raceId', raceId);
|
||||
url.searchParams.set('raceSlug', raceSlug);
|
||||
url.searchParams.set('raceName', raceName);
|
||||
url.searchParams.set('raceYear', raceYear);
|
||||
url.searchParams.set('raceMonthFolder', raceMonthFolder);
|
||||
url.searchParams.set('raceFolder', raceFolder);
|
||||
url.searchParams.set('lang', lang);
|
||||
url.searchParams.set('returnUrl', returnUrl);
|
||||
url.searchParams.set('devUserId', userId);
|
||||
url.searchParams.set('devDisplayName', displayName);
|
||||
url.searchParams.set('devEmail', email);
|
||||
url.searchParams.set('devMembershipStatus', membershipStatus);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getSearchArtifacts(searchId) {
|
||||
const searchRoot = path.join(SEARCH_LOG_ROOT, searchId);
|
||||
return {
|
||||
searchRoot,
|
||||
backendLogPath: path.join(LOG_ROOT, 'backend.log'),
|
||||
processorLogPath: path.join(LOG_ROOT, 'processor.log'),
|
||||
workerLogPath: path.join(searchRoot, 'worker.log'),
|
||||
matcherLogPath: path.join(searchRoot, 'matcher.log')
|
||||
};
|
||||
}
|
||||
|
||||
async function readUtf8(filePath) {
|
||||
return fs.readFile(filePath, 'utf8');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ROOT_DIR,
|
||||
LOG_ROOT,
|
||||
SEARCH_LOG_ROOT,
|
||||
FACEAI_BASE_URL,
|
||||
LEGACY_BASE_URL,
|
||||
LEGACY_HOME_URL,
|
||||
SIMULATOR_URL,
|
||||
SELFIE_NAME,
|
||||
EXPECTED_MATCH_COUNT,
|
||||
buildHandoffUrl,
|
||||
buildSimulatorUrl,
|
||||
dockerCompose,
|
||||
getSearchArtifacts,
|
||||
getSelfiePath,
|
||||
prepareHostState,
|
||||
readUtf8,
|
||||
runCommand,
|
||||
waitForHttp
|
||||
};
|
||||
27
faceai/tests/e2e/global-setup.js
Normal file
27
faceai/tests/e2e/global-setup.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
const {
|
||||
FACEAI_BASE_URL,
|
||||
SIMULATOR_URL,
|
||||
dockerCompose,
|
||||
prepareHostState,
|
||||
runCommand,
|
||||
waitForHttp
|
||||
} = require('./faceai-test-utils');
|
||||
|
||||
module.exports = async () => {
|
||||
await prepareHostState();
|
||||
await dockerCompose(['down', '-v', '--remove-orphans'], { allowFailure: true });
|
||||
await runCommand('npm', ['run', 'build']);
|
||||
await dockerCompose(['up', '--build', '-d']);
|
||||
|
||||
await waitForHttp(`${FACEAI_BASE_URL}/health`, ({ response, parsedBody }) => {
|
||||
return response.ok && parsedBody && parsedBody.ok === true;
|
||||
});
|
||||
|
||||
await waitForHttp(`${FACEAI_BASE_URL}/api/health/queue`, ({ response, parsedBody }) => {
|
||||
return response.ok && parsedBody && parsedBody.ok === true;
|
||||
});
|
||||
|
||||
await waitForHttp(SIMULATOR_URL, ({ response, bodyText }) => {
|
||||
return response.ok && bodyText.includes('FaceAI Legacy Simulator');
|
||||
});
|
||||
};
|
||||
9
faceai/tests/e2e/global-teardown.js
Normal file
9
faceai/tests/e2e/global-teardown.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const { dockerCompose } = require('./faceai-test-utils');
|
||||
|
||||
module.exports = async () => {
|
||||
if (process.env.FACEAI_E2E_KEEP_STACK === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
await dockerCompose(['down', '-v', '--remove-orphans'], { allowFailure: true });
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue