diff --git a/test/smoke/suites/debugConfiguration.test.ts b/test/smoke/suites/debugConfiguration.test.ts index 9aa879f40..1537a54d3 100644 --- a/test/smoke/suites/debugConfiguration.test.ts +++ b/test/smoke/suites/debugConfiguration.test.ts @@ -88,36 +88,32 @@ export function startDebugConfigurationTests(): void { await applicationInDirectModeButton.click(); await ElementHelper.waitPageLoad("domcontentloaded"); - // Retry reading launch.json content to account for async insertion timing - let launchContent: string | null | undefined = null; - let includesHermesConfig = false; - const maxAttempts = 20; // ~10s with 500ms waits - - for (let attempt = 0; attempt < maxAttempts; attempt++) { - const configurationElement = await ElementHelper.TryFindElement( - Element.configurationElementSelector, - TimeoutConstants.COMMAND_PALETTE_TIMEOUT, - ); - launchContent = await configurationElement?.textContent(); - if (launchContent) { - const normalized = launchContent.replace(/\s/g, ""); - if (normalized.includes("DebugAndroidHermes")) { - includesHermesConfig = true; - break; - } - } - await ElementHelper.waitPageLoad("networkidle"); - } - - if (!launchContent) { - assert.fail("Fail to set launch file configuration."); - } + const launchContent = await ComponentHelper.waitUntil( + async () => { + const configurationElement = await ElementHelper.TryFindElement( + Element.configurationElementSelector, + TimeoutConstants.COMMAND_PALETTE_TIMEOUT, + ); + const text = (await configurationElement?.textContent()) || ""; + const normalized = text.replace(/\s/g, ""); + + return { + ok: normalized.includes("DebugAndroidHermes"), + actual: normalized || "", + value: text, + }; + }, + { + operation: "launch.json debug configuration update", + expected: 'content includes "DebugAndroidHermes"', + timeout: TimeoutConstants.DEBUG_CONFIGURATION_TIMEOUT, + interval: 500, + }, + ); assert.ok( - includesHermesConfig, - `Expected launchContent to include "Debug Android Hermes", but got: ${( - launchContent || "" - ).replace(/\s/g, "")}`, + !!launchContent, + "Expected launch.json content to be available after configuration insertion.", ); }); }); diff --git a/test/smoke/suites/helper/componentHelper.ts b/test/smoke/suites/helper/componentHelper.ts index 04ca44148..9deaba801 100644 --- a/test/smoke/suites/helper/componentHelper.ts +++ b/test/smoke/suites/helper/componentHelper.ts @@ -98,28 +98,42 @@ export class ComponentHelper { expected: string, timeout: number = 30000, ): Promise { - const ok = await WaitHelper.waitIsTrue(async () => { - const packager = await this.getReactNativePackager(); - const currentState = await packager.getAttribute("aria-label"); - return !!currentState?.includes(expected); - }, timeout); - if (!ok) { - throw new Error(`Packager state did not include "${expected}" within ${timeout}ms`); - } + await this.waitUntil( + async () => { + const packager = await this.getReactNativePackager(); + const currentState = (await packager.getAttribute("aria-label")) || ""; + return { + ok: currentState.includes(expected), + actual: currentState, + }; + }, + { + operation: "packager state update", + expected: `state includes \"${expected}\"`, + timeout, + }, + ); } public static async waitPackagerStateIncludesOneOf( expectedList: string[], timeout: number = 30000, ): Promise { - const ok = await this.isPackagerStateIncludesOneOf(expectedList, timeout); - if (!ok) { - throw new Error( - `Packager state did not include any of ${expectedList - .map(e => `"${e}"`) - .join(", ")} within ${timeout}ms`, - ); - } + await this.waitUntil( + async () => { + const packager = await this.getReactNativePackager(); + const currentState = (await packager.getAttribute("aria-label")) || ""; + return { + ok: expectedList.some(exp => currentState.includes(exp)), + actual: currentState, + }; + }, + { + operation: "packager state update", + expected: `state includes one of ${expectedList.map(e => `\"${e}\"`).join(", ")}`, + timeout, + }, + ); } public static async isPackagerStateIncludesOneOf( @@ -132,4 +146,43 @@ export class ComponentHelper { return expectedList.some(exp => currentState?.includes(exp)); }, timeout); } + + public static async waitUntil( + condition: () => Promise<{ ok: boolean; actual?: string; value?: T }>, + options: { + operation: string; + expected: string; + timeout?: number; + interval?: number; + }, + ): Promise { + const timeout = options.timeout ?? 30000; + const interval = options.interval ?? 1000; + let lastActual = ""; + let lastValue: T | undefined; + + const ok = await WaitHelper.waitIsTrue( + async () => { + const result = await condition(); + if (result.actual !== undefined) { + lastActual = result.actual; + } + if (result.value !== undefined) { + lastValue = result.value; + } + + return result.ok; + }, + timeout, + interval, + ); + + if (!ok) { + throw new Error( + `[WaitTimeout] ${options.operation}. Expected: ${options.expected}. Last actual: ${lastActual}. Timeout: ${timeout}ms. Interval: ${interval}ms.`, + ); + } + + return lastValue; + } } diff --git a/test/smoke/suites/helper/timeoutConstants.ts b/test/smoke/suites/helper/timeoutConstants.ts index 643d6faa9..c92733919 100644 --- a/test/smoke/suites/helper/timeoutConstants.ts +++ b/test/smoke/suites/helper/timeoutConstants.ts @@ -15,6 +15,9 @@ export class TimeoutConstants { /** Command palette visibility timeout - 5 seconds */ static readonly COMMAND_PALETTE_TIMEOUT = 5000; + /** launch.json debug configuration insertion timeout - 10 seconds */ + static readonly DEBUG_CONFIGURATION_TIMEOUT = 10000; + /** Package loader timeout - 4 minutes (240 seconds) */ static readonly PACKAGE_LOADER_TIMEOUT = 4 * 60 * 1000;