From dd1abbf8732ca72ce081426dc0bc0e8eabdaf58e Mon Sep 17 00:00:00 2001 From: Maxim Dozhdev Date: Wed, 26 Aug 2026 13:23:18 +0200 Subject: [PATCH 1/3] test: keep the backend host across app relaunches processArguments in the session capabilities apply only to the first launch. reinstallApp and the other relaunch helpers call driver.activateApp, which starts the app with no environment, so E2E_LOCAL_HOST is lost and Env.swift falls back to the Info.plist value fixed at build time. Against a stack on another machine that means the app looks for Electrum on the simulator itself and never produces a balance, so completeOnboarding times out waiting for TotalBalance-primary. Every spec reinstalls in a before hook, so it affects all of them. Relaunch through `mobile: launchApp`, which does take an environment. Guarded on iOS and on the variable being set, so nothing changes for Android or for runs against a local stack. Co-Authored-By: Claude Opus 5 --- test/helpers/setup.ts | 20 +++++++++++++++++--- test/specs/migration.e2e.ts | 9 +++++---- test/specs/receive-ln-payments.e2e.ts | 3 ++- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/test/helpers/setup.ts b/test/helpers/setup.ts index 77b53c0..5ba8e5c 100644 --- a/test/helpers/setup.ts +++ b/test/helpers/setup.ts @@ -42,11 +42,25 @@ export function grantIOSCameraPermission(appIdParam?: string) { } } +export async function activateAppWithEnv(appId: string) { + // processArguments in the session capabilities only apply to the first launch, + // so a relaunch would lose E2E_LOCAL_HOST and the app would fall back to the + // Info.plist value baked in at build time. + if (driver.isIOS && process.env.E2E_LOCAL_HOST) { + await driver.execute('mobile: launchApp', { + bundleId: appId, + environment: { E2E_LOCAL_HOST: process.env.E2E_LOCAL_HOST }, + }); + return; + } + await driver.activateApp(appId); +} + export async function launchFreshApp() { const appId = getAppId(); await driver.terminateApp(appId); - await driver.activateApp(appId); + await activateAppWithEnv(appId); await sleep(3000); } @@ -62,7 +76,7 @@ export async function reinstallApp() { resetBootedIOSKeychain(); await driver.installApp(appPath); grantIOSCameraPermission(appId); - await driver.activateApp(appId); + await activateAppWithEnv(appId); } export function getRnAppPath(): string { @@ -92,7 +106,7 @@ export async function reinstallAppFromPath(appPath: string, appId: string = getA resetBootedIOSKeychain(); await driver.installApp(appPath); grantIOSCameraPermission(appId); - await driver.activateApp(appId); + await activateAppWithEnv(appId); } /** diff --git a/test/specs/migration.e2e.ts b/test/specs/migration.e2e.ts index 6dc165f..b41e7e5 100644 --- a/test/specs/migration.e2e.ts +++ b/test/specs/migration.e2e.ts @@ -29,6 +29,7 @@ import { grantIOSCameraPermission, reinstallAppFromPath, resetBootedIOSKeychain, + activateAppWithEnv, } from '../helpers/setup'; import { getAppId } from '../helpers/constants'; import initElectrum, { ElectrumClient } from '../helpers/electrum'; @@ -172,7 +173,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { console.info(`→ Installing native app from: ${getNativeAppPath()}`); await driver.installApp(getNativeAppPath()); grantIOSCameraPermission(); - await driver.activateApp(getAppId()); + await activateAppWithEnv(getAppId()); // Restore wallet with mnemonic (uses custom flow to handle backup sheet) await restoreWallet(mnemonic!, { @@ -197,7 +198,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { console.info(`→ Installing native app on top of RN: ${getNativeAppPath()}`); await driver.installApp(getNativeAppPath()); grantIOSCameraPermission(); - await driver.activateApp(getAppId()); + await activateAppWithEnv(getAppId()); // Handle migration flow await handleMigrationFlow({ withSweep: false }); @@ -217,7 +218,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { console.info(`→ Installing native app on top of RN: ${getNativeAppPath()}`); await driver.installApp(getNativeAppPath()); grantIOSCameraPermission(); - await driver.activateApp(getAppId()); + await activateAppWithEnv(getAppId()); // Handle migration flow await handleMigrationFlow({ withSweep: false }); @@ -239,7 +240,7 @@ describe('@migration - Migration from legacy RN app to native app', () => { console.info(`→ Installing native app on top of RN: ${getNativeAppPath()}`); await driver.installApp(getNativeAppPath()); grantIOSCameraPermission(); - await driver.activateApp(getAppId()); + await activateAppWithEnv(getAppId()); // Handle migration flow await handleMigrationFlow({ withSweep: false }); diff --git a/test/specs/receive-ln-payments.e2e.ts b/test/specs/receive-ln-payments.e2e.ts index cf5aa5d..9931634 100644 --- a/test/specs/receive-ln-payments.e2e.ts +++ b/test/specs/receive-ln-payments.e2e.ts @@ -22,6 +22,7 @@ import { } from '../helpers/actions'; import { payInvoice } from '../helpers/regtest'; import { getAppId } from '../helpers/constants'; +import { activateAppWithEnv } from '../helpers/setup'; const PAYMENT_COUNT = Number(process.env.PAYMENT_COUNT || '21'); const PAYMENT_AMOUNT = Number(process.env.PAYMENT_AMOUNT || '10'); @@ -39,7 +40,7 @@ function extractLightningInvoice(uri: string): string { describe('Receive LN payments (utility)', () => { before(async () => { const appId = getAppId(); - await driver.activateApp(appId); + await activateAppWithEnv(appId); await sleep(3000); }); From 2069431fdf2185b2927f55028642f1278bfdf23f Mon Sep 17 00:00:00 2001 From: Maxim Dozhdev Date: Wed, 26 Aug 2026 13:56:56 +0200 Subject: [PATCH 2/3] test: make WDA timeouts and appium logging configurable Session creation intermittently times out waiting for WebDriverAgent, and logLevel warn hides whether it is building, launching or failing to connect. Route the appium server log to artifacts and let the level and the WDA timeouts be raised per run. Defaults are unchanged. Co-Authored-By: Claude Opus 5 --- wdio.conf.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/wdio.conf.ts b/wdio.conf.ts index dc12f66..8bcdc16 100644 --- a/wdio.conf.ts +++ b/wdio.conf.ts @@ -18,6 +18,11 @@ const appiumNewCommandTimeout = Number.parseInt( process.env.APPIUM_NEW_COMMAND_TIMEOUT ?? '300', 10 ); +const wdaLaunchTimeout = Number.parseInt(process.env.WDA_LAUNCH_TIMEOUT ?? '300000', 10); +const connectionRetryTimeout = Number.parseInt( + process.env.WDIO_CONNECTION_RETRY_TIMEOUT ?? '360000', + 10 +); export const config: WebdriverIO.Config = { // @@ -108,8 +113,8 @@ export const config: WebdriverIO.Config = { // 🩹 Stability improvements 'appium:newCommandTimeout': 300, - 'appium:wdaLaunchTimeout': 300000, - 'appium:wdaConnectionTimeout': 300000, + 'appium:wdaLaunchTimeout': wdaLaunchTimeout, + 'appium:wdaConnectionTimeout': wdaLaunchTimeout, 'appium:wdaStartupRetries': 3, 'appium:wdaStartupRetryInterval': 5000, }, @@ -122,7 +127,7 @@ export const config: WebdriverIO.Config = { // Define all options that are relevant for the WebdriverIO instance here // // Level of logging verbosity: trace | debug | info | warn | error | silent - logLevel: 'warn', + logLevel: (process.env.WDIO_LOG_LEVEL as WebdriverIO.Config['logLevel']) ?? 'warn', // // Set specific log levels per logger // loggers: @@ -153,8 +158,8 @@ export const config: WebdriverIO.Config = { // // Default timeout in milliseconds for request // if browser driver or grid doesn't send response - // Must be >= wdaLaunchTimeout (300000) to allow WDA time to start - connectionRetryTimeout: 360000, + // Must be >= wdaLaunchTimeout to allow WDA time to start + connectionRetryTimeout, // // Default request retries count connectionRetryCount: 3, @@ -163,7 +168,11 @@ export const config: WebdriverIO.Config = { // Services take over a specific job you don't want to take care of. They enhance // your test setup with almost no effort. Unlike plugins, they don't add new // commands. Instead, they hook themselves up into the test process. - services: ['appium'], + services: [ + // Appium's own log is the only place that says whether WDA is building, + // launching or failing to connect. + ['appium', { logPath: process.env.APPIUM_LOG_PATH ?? './artifacts' }], + ], // Framework you want to run your specs with. // The following are supported: Mocha, Jasmine, and Cucumber From b28bfef908a2c68cc665a52f0e949d17b37fdcab Mon Sep 17 00:00:00 2001 From: Maxim Dozhdev Date: Wed, 26 Aug 2026 15:06:56 +0200 Subject: [PATCH 3/3] test: cover the reachable address in LND TLS cert LND issues its own cert on first start with SANs for 127.0.0.1, ::1 and its container address. Both gRPC and REST verify the hostname, so a suite running on another machine is rejected: ERR_TLS_CERT_ALTNAME_INVALID: IP 100.116.153.66 is not in the cert list: 127.0.0.1, ::1, 172.18.0.4 tlsextraip adds the address LND is actually reached on, reusing the variable externalip already takes. Defaults to 127.0.0.1, which is already covered, so a local stack is unaffected. Co-Authored-By: Claude Opus 5 --- docker/docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index dc57326..d2ea92b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -106,6 +106,10 @@ services: - '--noseedbackup' - '--alias=lnd' - '--externalip=${LND_EXTERNAL_IP:-127.0.0.1}' + # LND issues its own cert on first start, covering only 127.0.0.1, ::1 and + # its container address. gRPC and REST verify the hostname, so reaching it + # from another machine needs that address in the SAN list. + - '--tlsextraip=${LND_EXTERNAL_IP:-127.0.0.1}' - '--bitcoin.active' - '--bitcoin.regtest' - '--bitcoin.node=bitcoind'