Skip to content

Commit 6ccc939

Browse files
authored
Merge branch 'main' into fix/add-vs-currency-trending-list
2 parents b4c1391 + c901518 commit 6ccc939

75 files changed

Lines changed: 3068 additions & 552 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/smart-e2e-selection/action.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ inputs:
2727
required: false
2828
default: 'false'
2929
base-ref:
30-
description: 'PR base branch ref passed to the AI analysis script for diff comparison. When release/*, AI selection is skipped and the full E2E suite is selected.'
30+
description: 'PR base branch ref passed to the AI analysis script for diff comparison. When release/* or stable, AI selection is skipped and the full E2E suite is selected.'
3131
required: false
3232
default: ''
3333
outputs:
@@ -57,15 +57,15 @@ runs:
5757
echo "⏭️ SKIP=true due to 'skip-smart-e2e-selection' label on PR"
5858
fi
5959
60-
- name: Check release target branch (full E2E, no AI selection)
60+
- name: Check release or stable target branch (full E2E, no AI selection)
6161
id: check-release-target
6262
shell: bash
6363
run: |
6464
echo "SKIP=false" >> "$GITHUB_OUTPUT"
6565
BASE='${{ inputs.base-ref }}'
66-
if [[ -n "$BASE" && "$BASE" == release/* ]]; then
66+
if [[ -n "$BASE" && ( "$BASE" == release/* || "$BASE" == "stable" ) ]]; then
6767
echo "SKIP=true" >> "$GITHUB_OUTPUT"
68-
echo "⏭️ Base branch is release/* — skipping AI E2E selection; full E2E suite will run"
68+
echo "⏭️ Base branch is release/* or stable — skipping AI E2E selection; full E2E suite will run"
6969
fi
7070
7171
- name: Checkout for PR analysis
@@ -142,9 +142,9 @@ runs:
142142
echo "SKIP_REASON=skip-smart-e2e-selection label found" >> "$GITHUB_OUTPUT"
143143
echo "ai_confidence=100" >> "$GITHUB_OUTPUT"
144144
elif [[ "${{ steps.check-release-target.outputs.SKIP }}" == "true" ]]; then
145-
echo "⏭️ Skipping AI analysis - PR targets a release branch (release/*)"
145+
echo "⏭️ Skipping AI analysis - PR targets a release or stable branch"
146146
echo "SKIPPED=true" >> "$GITHUB_OUTPUT"
147-
echo "SKIP_REASON=PR targets a release branch (release/*)" >> "$GITHUB_OUTPUT"
147+
echo "SKIP_REASON=PR targets a release or stable branch (release/* or stable)" >> "$GITHUB_OUTPUT"
148148
echo "ai_confidence=100" >> "$GITHUB_OUTPUT"
149149
else
150150
echo "✅ Running AI analysis for PR #$PR_NUMBER"

.github/guidelines/E2E_DECISION_TREE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Flakiness detection is applied to modified E2E test files in PRs:
5656

5757
## Release branches
5858

59-
PRs to release branches (cherry-picked from main) are exempt from the following:
59+
PRs to release branches (cherry-picks from main to release/\* branches and PRs to stable branch) are exempt from the following:
6060

6161
- Label `pr-not-ready-for-e2e` is not applied
6262
- Smart AI E2E selection is skipped - all E2E suites are run (if changes are not ignorable-only, e.g. only docs)

.github/scripts/collect-qa-stats.mjs

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
*
2323
* MetaMetrics (top-level `metametrics` namespace): static scan of
2424
* `tests/helpers/analytics/expectations/*.ts` plus `LEGACY_INLINE_METAMETRICS_PATHS`
25-
* for specs not yet using declarative expectations.
25+
* for specs not yet using declarative expectations. Event names are picked from
26+
* `name:` fields, `eventNames: [...]`, `onboardingEvents.*`, and event-ish `const` arrays.
2627
*
2728
* Example output:
2829
* {
@@ -348,7 +349,63 @@ function parseConstStringLiterals(source) {
348349
}
349350

350351
/**
351-
* Event names from declarative `*.analytics.ts` modules (onboarding refs, `name:` entries, event arrays).
352+
* Content between "[" and matching "]" at the same nesting depth (naive bracket count).
353+
*
354+
* @param {string} source
355+
* @param {number} openBracketIdx index of '[' opening the array
356+
* @returns {string|null}
357+
*/
358+
function sliceBalancedSquareBracketInner(source, openBracketIdx) {
359+
if (source[openBracketIdx] !== '[') return null;
360+
let depth = 0;
361+
for (let i = openBracketIdx; i < source.length; i += 1) {
362+
const c = source[i];
363+
if (c === '[') depth += 1;
364+
else if (c === ']') {
365+
depth -= 1;
366+
if (depth === 0) return source.slice(openBracketIdx + 1, i);
367+
}
368+
}
369+
return null;
370+
}
371+
372+
/**
373+
* Segment from CSV inside `eventNames:` or event-ish `const` arrays. Spread/rest is skipped —
374+
* duplicated by a sibling `const *Names*` list when present (e.g. `...transactionEventNames`).
375+
*
376+
* @param {string} token
377+
* @param {Record<string, string>} onboardingMap
378+
* @param {Record<string, string>} strConsts
379+
* @returns {string|null}
380+
*/
381+
function resolveDeclarativeExpectationListToken(token, onboardingMap, strConsts) {
382+
const t = token.replace(/^\s+|\s+$/g, '');
383+
if (!t) return null;
384+
const lit = t.match(/^['"]([^'"]+)['"]$/);
385+
if (lit) return lit[1];
386+
const onb = t.match(/^onboardingEvents\.(\w+)$/);
387+
if (onb && onboardingMap[onb[1]]) return onboardingMap[onb[1]];
388+
if (/^\.\.\.\s*\w+$/.test(t)) return null;
389+
if (strConsts[t]) return strConsts[t];
390+
return null;
391+
}
392+
393+
/**
394+
* @param {string} inner
395+
* @param {Record<string, string>} onboardingMap
396+
* @param {Record<string, string>} strConsts
397+
* @param {Set<string>} out
398+
*/
399+
function collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out) {
400+
for (const part of inner.split(',')) {
401+
const v = resolveDeclarativeExpectationListToken(part, onboardingMap, strConsts);
402+
if (v) out.add(v);
403+
}
404+
}
405+
406+
/**
407+
* Event names from declarative `*.analytics.ts`: `eventNames:` arrays, onboarding refs,
408+
* `name:` entries, string/const lookups, and event-ish `const [...]` declarations.
352409
*
353410
* @param {string} source
354411
* @param {Record<string, string>} onboardingMap
@@ -368,11 +425,18 @@ function collectFromDeclarativeExpectationsSource(source, onboardingMap, out) {
368425
const v = onboardingMap[m[1]];
369426
if (v) out.add(v);
370427
}
371-
for (const m of source.matchAll(/\bname:\s*(\w+)\s*,/g)) {
428+
// Allow `name: IDENT,` (more properties follow) or `name: IDENT }` (single-field expectation object).
429+
for (const m of source.matchAll(/\bname:\s*(\w+)\s*[},]/g)) {
372430
const v = strConsts[m[1]];
373431
if (v) out.add(v);
374432
}
375433

434+
for (const em of source.matchAll(/\beventNames:\s*\[/g)) {
435+
const openIdx = em.index + em[0].length - 1;
436+
const inner = sliceBalancedSquareBracketInner(source, openIdx);
437+
if (inner) collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out);
438+
}
439+
376440
const reArrays = /\bconst\s+(\w+)\s*=\s*\[([\s\S]*?)\];/g;
377441
let am;
378442
while ((am = reArrays.exec(source)) !== null) {
@@ -382,21 +446,7 @@ function collectFromDeclarativeExpectationsSource(source, onboardingMap, out) {
382446
/(?:event|Event|Expected|expectation|analytics|Names)/.test(varName) ||
383447
/\bonboardingEvents\b|\bexpectedEvents\b/.test(inner);
384448
if (!looksLikeEventList) continue;
385-
for (const part of inner.split(',')) {
386-
const t = part.replace(/^\s+|\s+$/g, '');
387-
if (!t) continue;
388-
const lit = t.match(/^['"]([^'"]+)['"]$/);
389-
if (lit) {
390-
out.add(lit[1]);
391-
continue;
392-
}
393-
const onb = t.match(/^onboardingEvents\.(\w+)$/);
394-
if (onb && onboardingMap[onb[1]]) {
395-
out.add(onboardingMap[onb[1]]);
396-
continue;
397-
}
398-
if (strConsts[t]) out.add(strConsts[t]);
399-
}
449+
collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out);
400450
}
401451
}
402452

.github/workflows/auto-label-not-ready-for-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
types: [opened]
88
branches-ignore:
99
- 'release/**'
10+
- 'stable'
1011

1112
jobs:
1213
add-label:

app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ jest.mock('../../../../../core/Engine', () => ({
2424
},
2525
}));
2626

27+
const mockNavigationServiceNavigate = jest.fn();
28+
const mockNavigationServiceGoBack = jest.fn();
29+
jest.mock('../../../../../core/NavigationService', () => ({
30+
__esModule: true,
31+
default: {
32+
get navigation() {
33+
return {
34+
navigate: mockNavigationServiceNavigate,
35+
goBack: mockNavigationServiceGoBack,
36+
};
37+
},
38+
},
39+
}));
40+
2741
const mockNavigate = jest.fn();
2842
const mockGoBack = jest.fn();
2943
const mockReset = jest.fn();
@@ -442,6 +456,35 @@ describe('CardAuthentication Component', () => {
442456
});
443457
});
444458

459+
it('pops Card.ROOT off the root navigator on successful login when postAuthRedirect is set (no inner Card-stack reset, no cross-stack navigate)', async () => {
460+
mockRouteParams = {
461+
postAuthRedirect: {
462+
screen: Routes.MONEY.ROOT,
463+
params: { screen: Routes.MONEY.HOME },
464+
},
465+
};
466+
mockSubmitMutateAsync.mockResolvedValue({ done: true });
467+
render();
468+
const emailInput = screen.getByTestId('email-field');
469+
const passwordInput = screen.getByTestId('password-field');
470+
const loginButton = screen.getByTestId(
471+
CardAuthenticationSelectors.VERIFY_ACCOUNT_BUTTON,
472+
);
473+
474+
fireEvent.changeText(emailInput, 'test@example.com');
475+
fireEvent.changeText(passwordInput, 'password123');
476+
fireEvent.press(loginButton);
477+
478+
await waitFor(() => {
479+
expect(mockNavigationServiceGoBack).toHaveBeenCalledTimes(1);
480+
});
481+
// The origin (e.g. the Money tab) lives below Card.ROOT in the outer
482+
// navigator — popping reveals it without touching its own state or
483+
// doing a cross-stack navigate.
484+
expect(mockNavigationServiceNavigate).not.toHaveBeenCalled();
485+
expect(mockReset).not.toHaveBeenCalled();
486+
});
487+
445488
it('does not navigate when login error exists', () => {
446489
mockUseCardAuth.mockReturnValue(
447490
makeDefaultHookReturn({

app/components/UI/Card/Views/CardAuthentication/CardAuthentication.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { selectCardUserLocation } from '../../../../../selectors/cardController'
3030
import { CardMessageBoxType, type CardLocation } from '../../types';
3131
import { CardActions, CardScreens } from '../../util/metrics';
3232
import OnboardingStep from '../../components/Onboarding/OnboardingStep';
33+
import NavigationService from '../../../../../core/NavigationService';
3334
import { useTailwind } from '@metamask/design-system-twrnc-preset';
3435
import { countryCodeToFlag } from '../../util/countryCodeToFlag';
3536

@@ -41,7 +42,12 @@ const autoComplete = Platform.select<TextInputProps['autoComplete']>({
4142

4243
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
4344
type CardAuthenticationParams = {
44-
CardAuthentication: { showAuthPrompt?: boolean } | undefined;
45+
CardAuthentication:
46+
| {
47+
showAuthPrompt?: boolean;
48+
postAuthRedirect?: { screen: string; params?: object };
49+
}
50+
| undefined;
4551
};
4652

4753
const CardAuthentication = () => {
@@ -51,6 +57,7 @@ const CardAuthentication = () => {
5157
const route =
5258
useRoute<RouteProp<CardAuthenticationParams, 'CardAuthentication'>>();
5359
const showAuthPrompt = route.params?.showAuthPrompt ?? false;
60+
const postAuthRedirect = route.params?.postAuthRedirect;
5461
const [email, setEmail] = useState('');
5562
const [password, setPassword] = useState('');
5663
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
@@ -207,6 +214,11 @@ const CardAuthentication = () => {
207214
return;
208215
}
209216

217+
if (postAuthRedirect) {
218+
NavigationService.navigation?.goBack();
219+
return;
220+
}
221+
210222
// Successful login — navigate to home
211223
navigation.reset({
212224
index: 0,
@@ -228,6 +240,7 @@ const CardAuthentication = () => {
228240
dispatch,
229241
trackEvent,
230242
createEventBuilder,
243+
postAuthRedirect,
231244
],
232245
);
233246

0 commit comments

Comments
 (0)