import axios from "axios"; import { GenericContainer, StartedTestContainer } from "testcontainers"; import { Logger } from "@waku/utils"; const log = new Logger("container-helpers"); export interface ContainerSetupOptions { environment?: Record; networkMode?: string; timeout?: number; maxAttempts?: number; } export interface ContainerSetupResult { container: StartedTestContainer; baseUrl: string; } /** * Starts a waku-browser-tests Docker container with proper health checking. * Follows patterns from @waku/tests package for retry logic and cleanup. */ export async function startBrowserTestsContainer( options: ContainerSetupOptions = {} ): Promise { const { environment = {}, networkMode = "bridge", timeout = 2000, maxAttempts = 60 } = options; log.info("Starting waku-browser-tests container..."); let generic = new GenericContainer("waku-browser-tests:local") .withExposedPorts(8080) .withNetworkMode(networkMode); // Apply environment variables for (const [key, value] of Object.entries(environment)) { generic = generic.withEnvironment({ [key]: value }); } const container = await generic.start(); // Set up container logging - stream all output from the start const logs = await container.logs(); logs.on("data", (b) => process.stdout.write("[container] " + b.toString())); logs.on("error", (err) => log.error("[container log error]", err)); // Give container time to initialize await new Promise((r) => setTimeout(r, 5000)); const mappedPort = container.getMappedPort(8080); const baseUrl = `http://127.0.0.1:${mappedPort}`; // Wait for server readiness with retry logic (following waku/tests patterns) const serverReady = await waitForServerReady(baseUrl, maxAttempts, timeout); if (!serverReady) { await logFinalContainerState(container); throw new Error("Container failed to become ready"); } log.info("✅ Browser tests container ready"); await new Promise((r) => setTimeout(r, 500)); // Final settling time return { container, baseUrl }; } /** * Waits for server to become ready with exponential backoff and detailed logging. * Follows retry patterns from @waku/tests ServiceNode. */ async function waitForServerReady( baseUrl: string, maxAttempts: number, timeout: number ): Promise { for (let i = 0; i < maxAttempts; i++) { try { const res = await axios.get(`${baseUrl}/`, { timeout }); if (res.status === 200) { log.info(`Server is ready after ${i + 1} attempts`); return true; } } catch (error) { if (i % 10 === 0) { log.info(`Attempt ${i + 1}/${maxAttempts} failed:`, error.code || error.message); } } await new Promise((r) => setTimeout(r, 1000)); } return false; } /** * Logs final container state for debugging, following waku/tests error handling patterns. */ async function logFinalContainerState(container: StartedTestContainer): Promise { try { const finalLogs = await container.logs({ tail: 50 }); log.info("=== Final Container Logs ==="); finalLogs.on("data", (b) => log.info(b.toString())); await new Promise((r) => setTimeout(r, 1000)); } catch (logError) { log.error("Failed to get container logs:", logError); } } /** * Gracefully stops containers with retry logic, following teardown patterns from waku/tests. */ export async function stopContainer(container: StartedTestContainer): Promise { if (!container) return; log.info("Stopping container gracefully..."); try { await container.stop({ timeout: 10000 }); log.info("Container stopped successfully"); } catch (error) { const message = error instanceof Error ? error.message : String(error); log.warn( "Container stop had issues (expected):", message ); } }