Compare commits

...

5 Commits

Author SHA1 Message Date
Overtorment
e3231548cd TST: fix android e2e 2026-05-16 12:58:19 +01:00
Overtorment
3863cbfb7d TST: fix android e2e 2026-05-16 12:56:08 +01:00
Overtorment
a05abde5d2 TST: fix android e2e 2026-05-16 11:09:34 +01:00
Overtorment
54b2610bdc Merge remote-tracking branch 'origin/master' into tst-fix-android-e2e 2026-05-16 10:59:13 +01:00
Overtorment
db17c1b49d TST: fix android e2e 2026-05-15 22:00:29 +01:00
4 changed files with 77 additions and 46 deletions

View File

@ -33,7 +33,8 @@
"simulator": {
"type": "ios.simulator",
"device": {
"type": "iPhone 17"
"type": "iPhone 17",
"os": "iOS 26.4"
}
},
"emulator": {

View File

@ -119,35 +119,42 @@ describe('BlueWallet UI Tests - no wallets', () => {
// network -> electrum server
// change electrum server to electrum.blockstream.info and revert it back
// skip this test on iOS. HeaderMenuButton tap triggers a keyboard open for some reason.
if (device.getPlatform() === 'andoid') {
// Skipped on iOS: tapping HeaderMenuButton steals focus and reopens the
// soft keyboard, which the iOS Electrum form isn't structured to recover
// from cleanly. Android-only for now.
if (device.getPlatform() === 'android') {
await element(by.id('ElectrumSettings')).tap();
await waitFor(element(by.id('HostInput')))
.toBeVisible()
.whileElement(by.id('ElectrumSettingsScrollView'))
.scroll(500, 'down'); // in case emu screen is small and it doesnt fit
await element(by.id('HostInput')).replaceText('electrum.blockstream.info\n');
await element(by.id('HostInput')).tapReturnKey();
await waitForKeyboardToClose();
await element(by.id('PortInput')).replaceText('50001\n');
await element(by.id('PortInput')).tapReturnKey();
await waitForKeyboardToClose();
await waitFor(element(by.id('Save')))
.toBeVisible()
.whileElement(by.id('ElectrumSettingsScrollView'))
.scroll(500, 'down'); // in case emu screen is small and it doesnt fit
// CI runs Android with a hardware keyboard (waitForKeyboardToClose is a
// no-op there), so the form layout shifts unpredictably and inputs that
// are nominally on screen can fall just under Detox's 75% visibility
// threshold. Scroll each control into view before touching it, and
// commit via the explicit Save button rather than IME return-key taps
// (which require focus state we can't reliably guarantee here).
const scrollIntoView = id =>
waitFor(element(by.id(id)))
.toBeVisible()
.whileElement(by.id('ElectrumSettingsScrollView'))
.scroll(300, 'down');
await scrollIntoView('HostInput');
await element(by.id('HostInput')).replaceText('electrum.blockstream.info');
await scrollIntoView('PortInput');
await element(by.id('PortInput')).replaceText('50001');
await scrollIntoView('Save');
await element(by.id('Save')).tap();
await waitForText('OK');
await element(by.text('OK')).tap();
await element(by.id('HeaderMenuButton')).tap();
await element(by.text('Reset to default')).tap();
await element(by.text('RESET TO DEFAULT')).tap();
await waitForText('OK');
await element(by.text('OK')).tap();
await waitFor(element(by.id('HostInput')))
.toBeVisible()
.whileElement(by.id('ElectrumSettingsScrollView'))
.scroll(500, 'down'); // in case emu screen is small and it doesnt fit
await scrollIntoView('HostInput');
await expect(element(by.id('HostInput'))).toHaveText('');
await expect(element(by.id('PortInput'))).toHaveText('');
await expect(element(by.id('SSLPortInput'))).toHaveToggleValue(false);
@ -182,28 +189,30 @@ describe('BlueWallet UI Tests - no wallets', () => {
await waitFor(element(by.id('NotificationsSwitch')))
.toBeVisible()
.withTimeout(10000);
await element(by.id('NotificationsSwitch')).tap();
// On iOS 26 the simulator's notifications permission grant from
// `device.launchApp({ permissions: { notifications: 'YES' } })` no longer
// persists, so the app's "You have denied permission" alert reliably
// appears — but its render timing varies, sometimes landing well after
// the toggle settles. Use a generous window and re-check after each
// toggle so the alert never leaks past `goBack()`.
const dismissOKAlertIfPresent = async (timeoutMs = 15000) => {
try {
await waitFor(element(by.text('OK')))
.toBeVisible()
.withTimeout(timeoutMs);
await element(by.text('OK')).tap();
} catch (_) {
// Alert not shown, which is fine - notifications might be enabled.
}
};
// If notifications are not enabled on the device, an alert will appear
try {
await waitFor(element(by.text('OK')))
.toBeVisible()
.withTimeout(3000);
await element(by.text('OK')).tap();
} catch (_) {
// Alert not shown, which is fine - notifications might be enabled
}
await element(by.id('NotificationsSwitch')).tap();
// If notifications are not enabled on the device, an alert will appear
try {
await waitFor(element(by.text('OK')))
.toBeVisible()
.withTimeout(3000);
await element(by.text('OK')).tap();
} catch (_) {
// Alert not shown, which is fine - notifications might be enabled
}
await dismissOKAlertIfPresent();
await element(by.id('NotificationsSwitch')).tap();
await dismissOKAlertIfPresent();
// Belt + suspenders: occasionally the alert lands just after the second
// dismissal check has resolved; sweep once more with a short timeout.
await dismissOKAlertIfPresent(3000);
await goBack();
await goBack();
@ -753,12 +762,16 @@ describe('BlueWallet UI Tests - no wallets', () => {
await sleep(1000);
await element(by.text('OK')).tap();
// wait for discovery to be completed
await waitFor(element(by.text("m/84'/0'/0'")))
// wait for discovery to be completed.
// Discovery now surfaces the same path under multiple wallet kinds (e.g.
// `m/84'/0'/0'` appears under both BIP84 SegWit and BIP44 Legacy rows), so
// `by.text(…)` matches more than one element. Pin to atIndex(0) — we only
// need to confirm any one match rendered.
await waitFor(element(by.text("m/84'/0'/0'")).atIndex(0))
.toBeVisible()
.withTimeout(600 * 1000);
await expect(element(by.text("m/44'/0'/1'"))).toBeVisible();
await expect(element(by.text("m/49'/0'/0'"))).toBeVisible();
await expect(element(by.text("m/44'/0'/1'")).atIndex(0)).toBeVisible();
await expect(element(by.text("m/49'/0'/0'")).atIndex(0)).toBeVisible();
await expect(element(by.id('Loading'))).not.toBeVisible();
// open custom derivation path screen and import the wallet

View File

@ -702,7 +702,9 @@ describe('BlueWallet UI Tests - import BIP84 wallet', () => {
await element(by.id('FreezeSwitch')).tap(); // freeze switch
await waitForSwitchValue('FreezeSwitch', true);
await element(by.id('CoinControlOutputDone')).tap();
await waitFor(element(by.id('CoinControlOutputDone'))).not.toBeVisible().withTimeout(20000);
await waitFor(element(by.id('CoinControlOutputDone')))
.not.toBeVisible()
.withTimeout(20000);
await expect(element(by.id('OutputMemoLabel').and(by.text('Test2')))).toBeVisible();
await expect(element(by.id('FrozenBadge'))).toBeVisible();

View File

@ -361,6 +361,13 @@ export async function goBack() {
// Try each back/close affordance in order; retry the full set up to 10 times.
const candidates = [by.id('BackButton'), by.id('NavigationCloseButton'), by.label('Back'), by.text('Close')];
// iOS 26 trap: a leaked UIAlertController dims the window with
// `_alertControllerDimmingViewColor` and intercepts every hit-test, so none of
// the back/close candidates can be tapped until the alert itself is dismissed.
// When the primary pass fails, fall through to OK/Cancel to clear the alert,
// then let the next loop iteration retry back/close.
const alertDismissals = [by.text('OK'), by.text('Cancel')];
for (let attempt = 0; attempt < 10; attempt++) {
for (const matcher of candidates) {
try {
@ -370,6 +377,14 @@ export async function goBack() {
/* try next */
}
}
for (const matcher of alertDismissals) {
try {
await element(matcher).atIndex(0).tap();
break;
} catch (_) {
/* try next */
}
}
await sleep(500);
}