Skip to content

Commit 9b49f8e

Browse files
committed
fix(e2e-native): stabilize tradingBuyFlow test
1 parent e3594d0 commit 9b49f8e

File tree

7 files changed

+94
-16
lines changed

7 files changed

+94
-16
lines changed

suite-native/app/e2e/pageObjects/trading/TradingFormActions.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect as detoxExpect } from 'detox';
22

33
import { TradingActions } from './TradingActions';
4-
import { wait, waitForVisible } from '../../support/utils';
4+
import { wait, waitForVisible, waitToHaveRegex } from '../../support/utils';
55

66
export abstract class TradingFormActions extends TradingActions {
77
abstract waitForQuotesToLoad(): Promise<void>;
@@ -22,6 +22,10 @@ export abstract class TradingFormActions extends TradingActions {
2222
return this.getElementById('fiat-amount-input');
2323
}
2424

25+
getReceiveCryptoAmountElement() {
26+
return this.getElementById('crypto-amount-input');
27+
}
28+
2529
getSendCryptoAmountElement() {
2630
return this.getElementById('send-amount-input');
2731
}
@@ -39,6 +43,7 @@ export abstract class TradingFormActions extends TradingActions {
3943
await this.expectSheetHeaderTitle('Currency');
4044
await this.getSearchFiatElement().replaceText(fiatCurrency.slice(0, -1));
4145
await wait(this.BOTTOM_SHEET_ANIMATION_DURATION);
46+
await waitForVisible(by.text(fiatCurrency));
4247
await element(by.text(fiatCurrency)).tap();
4348

4449
await waitFor(this.getElementById('fiat-button/ticker'))
@@ -53,6 +58,7 @@ export abstract class TradingFormActions extends TradingActions {
5358

5459
await this.expectSheetHeaderTitle('Country of residence');
5560
await this.getSearchCountryElement().replaceText(countrySearch);
61+
await waitForVisible(by.text(country));
5662
await element(by.text(country)).tap();
5763

5864
await waitFor(this.getElementById('country/value'))
@@ -92,7 +98,9 @@ export abstract class TradingFormActions extends TradingActions {
9298

9399
async setFiatAmount(amount: string) {
94100
await this.getFiatAmountElement().replaceText(amount);
95-
await wait(100);
101+
const nonZeroRegex = /^(?!0+(?:\.0+)?$)\d+(\.\d+)?$/;
102+
// await wait(999999);
103+
await waitToHaveRegex(this.getReceiveCryptoAmountElement(), nonZeroRegex); // wait until the crypto amount is updated
96104
await this.getFiatAmountElement().tapReturnKey();
97105
await this.waitForQuotesToLoad();
98106
}
@@ -133,7 +141,7 @@ export abstract class TradingFormActions extends TradingActions {
133141
await networkFilterTab.tap();
134142
}
135143

136-
await waitForVisible(by.text(asset), { timeout: this.BOTTOM_SHEET_ANIMATION_DURATION });
144+
await waitForVisible(by.text(asset));
137145
await element(by.text(asset)).tap();
138146

139147
await waitFor(this.getElementById('asset-receive-button/symbol'))

suite-native/app/e2e/support/utils.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import path from 'node:path';
22

3-
const isIndexableNativeElement = (
4-
v: Detox.IndexableNativeElement | Detox.NativeMatcher,
5-
): v is Detox.IndexableNativeElement => {
3+
import { scheduleAction } from '@trezor/utils';
4+
5+
type ElementAttributes = {
6+
text?: string;
7+
label?: string;
8+
value?: string;
9+
};
10+
11+
type ElementOrMatcher = Detox.IndexableNativeElement | Detox.NativeMatcher;
12+
13+
const isIndexableNativeElement = (v: ElementOrMatcher): v is Detox.IndexableNativeElement => {
614
const anyV = v as any;
715

816
return !!anyV && (typeof anyV.tap === 'function' || typeof anyV.atIndex === 'function');
@@ -14,6 +22,11 @@ export const platform = device.getPlatform();
1422
// On the other hand, if we are trying to scroll to 100% visibility on iOS, it causes scrolling more than height of the screen and it makes Detox crash.
1523
const SCROLL_VISIBILITY_THRESHOLD = platform === 'android' ? 100 : undefined;
1624

25+
export const RETRY_CONF = {
26+
attempts: 5,
27+
gap: 500,
28+
};
29+
1730
export const wait = async (ms: number) => {
1831
await new Promise(resolve => setTimeout(resolve, ms));
1932
};
@@ -40,13 +53,17 @@ function pruneToAppStack(invocationStack: string): string {
4053
return kept.length ? ['Error'].concat(kept).join('\n') : invocationStack;
4154
}
4255

56+
function getTarget(elementOrMatcher: ElementOrMatcher) {
57+
return isIndexableNativeElement(elementOrMatcher)
58+
? elementOrMatcher
59+
: element(elementOrMatcher);
60+
}
61+
4362
export const waitForVisible = async (
44-
elementOrMatcher: Detox.IndexableNativeElement | Detox.NativeMatcher,
63+
elementOrMatcher: ElementOrMatcher,
4564
{ timeout = 30_000 }: { timeout?: number } = {},
4665
) => {
47-
const target = isIndexableNativeElement(elementOrMatcher)
48-
? elementOrMatcher
49-
: element(elementOrMatcher);
66+
const target = getTarget(elementOrMatcher);
5067
const invocationStack = new Error().stack || '';
5168
try {
5269
await waitFor(target).toBeVisible().withTimeout(timeout);
@@ -58,6 +75,34 @@ export const waitForVisible = async (
5875
}
5976
};
6077

78+
export const waitToHaveRegex = async (
79+
elementOrMatcher: ElementOrMatcher,
80+
expectedRegex: RegExp,
81+
{
82+
timeout = 30_000,
83+
attributeType = 'text',
84+
}: { timeout?: number; attributeType?: 'text' | 'label' | 'value' } = {},
85+
) => {
86+
const target = getTarget(elementOrMatcher);
87+
await waitForVisible(target, { timeout });
88+
await scheduleAction(async () => {
89+
const attributes = await target.getAttributes();
90+
const testedAttribute = (attributes as ElementAttributes)[attributeType];
91+
92+
if (typeof testedAttribute !== 'string') {
93+
throw new Error(
94+
`waitForRegex(): could not get "${attributeType}" property from the element`,
95+
);
96+
}
97+
98+
if (!expectedRegex.test(testedAttribute)) {
99+
throw new Error(
100+
`waitForRegex(): target text "${testedAttribute}" not matching ${expectedRegex} after ${timeout}ms`,
101+
);
102+
}
103+
}, RETRY_CONF);
104+
};
105+
61106
export const appIsFullyLoaded = async () => {
62107
await waitForVisible(by.id('@screen/mainScrollView'), { timeout: 35_000 });
63108
};
@@ -85,9 +130,7 @@ export const inputTextToElement = async (element: Detox.IndexableNativeElement,
85130
};
86131

87132
// Use this method in absolute necessity only! Try-catch in tests is discouraged because it may hide real issues.
88-
export const isElementVisible = async (
89-
elementOrMatcher: Detox.IndexableNativeElement | Detox.NativeMatcher,
90-
): Promise<boolean> => {
133+
export const isElementVisible = async (elementOrMatcher: ElementOrMatcher): Promise<boolean> => {
91134
const target = isIndexableNativeElement(elementOrMatcher)
92135
? elementOrMatcher
93136
: element(elementOrMatcher);
@@ -99,3 +142,20 @@ export const isElementVisible = async (
99142
return false;
100143
}
101144
};
145+
146+
export function testWithRepeat(
147+
runs: number,
148+
name: string,
149+
testFunction: () => Promise<void> | void,
150+
timeout?: number,
151+
) {
152+
for (let i = 1; i <= runs; i++) {
153+
it(
154+
name,
155+
async () => {
156+
await testFunction();
157+
},
158+
timeout,
159+
);
160+
}
161+
}

suite-native/app/e2e/tests/deviceSettingsT3T1.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ conditionalDescribe(
2222
beforeEach(async () => {
2323
await openApp({ args: { preloadedState: preloadedStateT3T1 } });
2424
await prepareTrezorEmulator({ model: 'T3T1' });
25-
await waitForVisible(by.id('@device-manager/connection-status'));
25+
await onDeviceManager.assertDeviceSwitcherState({ title: 'Connected' });
2626
await onDeviceManager.tapDeviceSwitch();
2727
await onDeviceManager.tapDeviceSettingsButton();
2828
});

suite-native/app/e2e/tests/tradingBuyFlow.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ const preloadedState = preparePreloadedReduxState(
1111
onboardingCompletedState,
1212
);
1313

14-
// FIXME https://github.com/trezor/trezor-suite/issues/23518
15-
describe.skip('Trade Buy [@noDevice]', () => {
14+
describe('Trade Buy [@noDevice]', () => {
1615
beforeEach(async () => {
1716
await openApp({ args: { preloadedState } });
1817
await onHome.assertIsPortfolioGraphVisible();

suite-native/app/eslint.config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { eslint } from '@trezor/eslint';
2+
3+
export default [
4+
...eslint,
5+
{
6+
files: ['**/e2e/support/**'],
7+
rules: { 'jest/no-export': 'off' },
8+
},
9+
];

suite-native/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@
171171
"@suite-common/test-utils": "workspace:^",
172172
"@suite-native/test-utils": "workspace:*",
173173
"@trezor/connect-mobile": "workspace:^",
174+
"@trezor/eslint": "workspace:^",
174175
"@types/node": "22.13.10",
175176
"@types/react-native-get-random-values": "1.8.2",
176177
"babel-jest": "29.7.0",

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11844,6 +11844,7 @@ __metadata:
1184411844
"@trezor/connect": "workspace:*"
1184511845
"@trezor/connect-mobile": "workspace:^"
1184611846
"@trezor/e2e-utils": "workspace:*"
11847+
"@trezor/eslint": "workspace:^"
1184711848
"@trezor/react-native-usb": "workspace:*"
1184811849
"@trezor/styles": "workspace:*"
1184911850
"@trezor/theme": "workspace:*"

0 commit comments

Comments
 (0)