Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
60 changes: 59 additions & 1 deletion test/helpers/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,10 @@ export async function waitForTextToDisappear(texts: string[], timeout: number) {

async function assertAddressTypeSwitchFeedback() {
// await waitForToast('AddressTypeApplyingToast', { dismiss: false });
await waitForToast('AddressTypeSettingsUpdatedToast');
await waitForToast('AddressTypeSettingsUpdatedToast', {
dismiss: driver.isAndroid,
timeout: 120_000,
});
}

export async function switchPrimaryAddressType(nextType: addressTypePreference) {
Expand Down Expand Up @@ -1265,6 +1268,61 @@ export async function waitForToast(
}
}

async function waitForTransientToastAfterAction(
toastId: ToastId,
action: () => Promise<void>
) {
if (driver.isAndroid) {
await action();
await waitForToast(toastId);
return;
}

// These feedback toasts live for 1.5 seconds. XCUITest's default all-match lookup can
// find one, then lose it while rebinding the accessibility snapshot. Scope the faster
// single-match lookup to this top-level element so normal nested lookups stay unchanged.
await driver.updateSettings({ useFirstMatch: true });
try {
await browser.waitUntil(
async () => {
await action();
try {
const toast = await elementById(toastId);
return Boolean(toast.elementId);
} catch {
return false;
}
},
{
timeout: 30_000,
interval: 250,
timeoutMsg: `Timed out waiting for transient toast: ${toastId}`,
}
);
} finally {
await driver.updateSettings({ useFirstMatch: false });
}
}

export async function exceedAmountInputCap(maxAmountSats: number) {
await enterAmount(maxAmountSats);
await verifyAmountToSend(maxAmountSats);
await waitForTransientToastAfterAction('SendAmountExceededToast', async () => {
await tap('N1');
});
await verifyAmountToSend(maxAmountSats);
}

export async function exceedAvailableAmountInputCap() {
await tap('AvailableAmount');
const availableAmountSats = await getAmountUnder('AvailableAmount');
await verifyAmountToSend(availableAmountSats);
await waitForTransientToastAfterAction('SendAmountExceededToast', async () => {
await tap('N1');
});
await verifyAmountToSend(availableAmountSats);
}

/** Acknowledges the received payment notification by tapping the button.
*/
export async function acknowledgeReceivedPayment({ timeout = 30_000 }: { timeout?: number } = {}) {
Expand Down
20 changes: 17 additions & 3 deletions test/helpers/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}

/**
Expand Down
17 changes: 5 additions & 12 deletions test/specs/lnurl.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
acknowledgeReceivedPayment,
acknowledgeExternalSuccess,
enterAmount,
exceedAmountInputCap,
} from '../helpers/actions';
import { reinstallApp } from '../helpers/setup';
import { ciIt } from '../helpers/suite';
Expand Down Expand Up @@ -92,7 +93,7 @@ describe('@lnurl - LNURL', () => {
lightning: {
backend: 'lnd',
config: {
hostname: '127.0.0.1:8080',
hostname: `${lndConfig.restHost}:${lndConfig.restPort}`,
macaroon: lndConfig.macaroonPath,
cert: lndConfig.tls,
},
Expand Down Expand Up @@ -175,18 +176,10 @@ describe('@lnurl - LNURL', () => {

await enterAddressViaScanPrompt(payRequest1.encoded, { acceptCameraPermission: false });
await expectTextWithin('SendNumberField', '0');
// Check that 149 sats is below minimum and 201 sats is above maximum (both rejected)
try {
await enterAmount(201);
await waitForToast('SendAmountExceededToast', { dismiss: driver.isAndroid });
} catch {
console.warn('SendAmountExceededToast not triggered, trying again...');
// tap on 1 fast to trigger the toast
await elementById('N1').click();
await waitForToast('SendAmountExceededToast', { dismiss: driver.isAndroid });
}
// Check that input above the 200 sat maximum is capped and 149 sats is rejected as below minimum
await exceedAmountInputCap(200);

await multiTap('NRemove', 3); // remove "201"
await multiTap('NRemove', 3); // remove "200"
await enterAmount(149);
await expectTextWithin('SendNumberField', '149');
await tap('ContinueAmount');
Expand Down
9 changes: 5 additions & 4 deletions test/specs/migration.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
grantIOSCameraPermission,
reinstallAppFromPath,
resetBootedIOSKeychain,
activateAppWithEnv,
} from '../helpers/setup';
import { getAppId } from '../helpers/constants';
import initElectrum, { ElectrumClient } from '../helpers/electrum';
Expand Down Expand Up @@ -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!, {
Expand All @@ -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 });
Expand All @@ -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 });
Expand All @@ -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 });
Expand Down
3 changes: 2 additions & 1 deletion test/specs/receive-ln-payments.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);
});

Expand Down
20 changes: 3 additions & 17 deletions test/specs/send.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
editRecipientAddress,
typeRecipientInput,
tap,
enterAmount,
exceedAvailableAmountInputCap,
verifyAmountToSend,
} from '../helpers/actions';
import { lndConfig } from '../helpers/constants';
Expand Down Expand Up @@ -145,14 +145,7 @@ describe('@send - Send', () => {

// type amount over balance and verify you cannot continue
await tap('AddressContinue');
await enterAmount(amount + 1);
try {
await waitForToast('SendAmountExceededToast');
} catch {
console.warn('SendAmountExceededToast not triggered, trying again...');
await elementById('N1').click();
await waitForToast('SendAmountExceededToast');
}
await exceedAvailableAmountInputCap();
await tap('NavigationBack');

// check validation for unified invoice when balance is enough (10_000 sats)
Expand Down Expand Up @@ -265,14 +258,7 @@ describe('@send - Send', () => {
const { paymentRequest: invoice0 } = await lnd.addInvoice({});
console.info({ invoice0 });
await enterAddress(invoice0);
await enterAmount(10_000 + 1);
try {
await waitForToast('SendAmountExceededToast');
} catch {
console.warn('SendAmountExceededToast not triggered, trying again...');
await elementById('N1').click();
await waitForToast('SendAmountExceededToast');
}
await exceedAvailableAmountInputCap();
await swipeFullScreen('down');

// send to onchain address
Expand Down
21 changes: 15 additions & 6 deletions wdio.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
//
Expand Down Expand Up @@ -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,
},
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down