Skip to content

Commit b5d0149

Browse files
authored
Merge branch 'main' into harry/MMQA-1711
2 parents 5057f1e + aac019d commit b5d0149

11 files changed

Lines changed: 330 additions & 32 deletions

File tree

.github/guidelines/E2E_DECISION_TREE.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ To save infra resources while waiting for static analysis findings and potential
3333
- E2E tests are skipped and merge is blocked while the label is present, **unless** all changes are ignorable-only.
3434
- If E2E tests are needed, they should pass to be able to merge.
3535

36-
## AI test selection
36+
## Smart AI E2E test selection
3737

3838
Runs only when all of the following are true:
3939

@@ -53,3 +53,10 @@ Flakiness detection is applied to modified E2E test files in PRs:
5353
- Modified E2E test files run twice
5454
- It applies to existing test files as well as new test files added in the PR
5555
- It can be disabled by adding the label `skip-e2e-flakiness-detection`. Useful when making large refactors or when changes don't pose flakiness risk.
56+
57+
## Release branches
58+
59+
PRs to release branches (cherry-picked from main) are exempt from the following:
60+
61+
- Label `pr-not-ready-for-e2e` is not applied
62+
- Smart AI E2E selection is skipped - all E2E suites are run (if changes are not ignorable-only, e.g. only docs)

app/components/UI/Identity/BackupAndSyncFeaturesToggles/BackupAndSyncFeaturesToggles.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React, { useCallback, useMemo } from 'react';
22
import { View, Switch, InteractionManager } from 'react-native';
33

4-
import Text, {
4+
import {
5+
Text,
56
TextColor,
67
TextVariant,
7-
} from '../../../../component-library/components/Texts/Text';
8+
Icon,
9+
IconName,
10+
} from '@metamask/design-system-react-native';
811
import { useTheme } from '../../../../util/theme';
912
import styles from './BackupAndSyncFeaturesToggles.styles';
1013
import { useBackupAndSync } from '../../../../util/identity/hooks/useBackupAndSync';
@@ -16,9 +19,6 @@ import {
1619
selectIsBackupAndSyncUpdateLoading,
1720
} from '../../../../selectors/identity';
1821
import { BACKUPANDSYNC_FEATURES } from '@metamask/profile-sync-controller/user-storage';
19-
import Icon, {
20-
IconName,
21-
} from '../../../../component-library/components/Icons/Icon';
2222
import { strings } from '../../../../../locales/i18n';
2323
import { MetaMetricsEvents } from '../../../../core/Analytics';
2424
import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
@@ -126,10 +126,10 @@ const BackupAndSyncFeaturesToggles = () => {
126126
return (
127127
<View style={styles.setting}>
128128
<View style={styles.heading}>
129-
<Text variant={TextVariant.HeadingSM}>
129+
<Text variant={TextVariant.HeadingSm}>
130130
{strings('backupAndSync.manageWhatYouSync.title')}
131131
</Text>
132-
<Text variant={TextVariant.BodySM} color={TextColor.Alternative}>
132+
<Text variant={TextVariant.BodySm} color={TextColor.TextAlternative}>
133133
{strings('backupAndSync.manageWhatYouSync.description')}
134134
</Text>
135135
</View>

app/components/UI/Identity/BackupAndSyncToggle/BackupAndSyncToggle.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import React, { useCallback, useEffect } from 'react';
33
import { View, Switch, Linking, InteractionManager } from 'react-native';
44
// import { useNavigation } from '@react-navigation/native';
55

6-
import Text, {
6+
import {
7+
Text,
78
TextVariant,
89
TextColor,
9-
} from '../../../../component-library/components/Texts/Text';
10+
} from '@metamask/design-system-react-native';
1011
import { useTheme } from '../../../../util/theme';
1112
// import { strings } from '../../../../../locales/i18n';
1213
import styles from './BackupAndSyncToggle.styles';
@@ -150,7 +151,7 @@ const BackupAndSyncToggle = ({
150151
return (
151152
<View style={styles.setting}>
152153
<View style={styles.heading}>
153-
<Text variant={TextVariant.HeadingSM}>
154+
<Text variant={TextVariant.HeadingSm}>
154155
{strings('backupAndSync.title')}
155156
</Text>
156157
<Switch
@@ -165,9 +166,9 @@ const BackupAndSyncToggle = ({
165166
testID={BACKUP_AND_SYNC_TOGGLE_TEST_IDS.TOGGLE}
166167
/>
167168
</View>
168-
<Text variant={TextVariant.BodyMD} color={TextColor.Alternative}>
169+
<Text variant={TextVariant.BodyMd} color={TextColor.TextAlternative}>
169170
{strings('backupAndSync.enable.description')}
170-
<Text color={TextColor.Info} onPress={handleLink}>
171+
<Text color={TextColor.InfoDefault} onPress={handleLink}>
171172
{strings('backupAndSync.privacyLink')}
172173
</Text>
173174
</Text>

app/components/UI/Identity/ConfirmTurnOnBackupAndSyncModal/ConfirmTurnOnBackupAndSyncModal.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import React, { useRef } from 'react';
22

3-
import BottomSheet, {
4-
BottomSheetRef,
5-
} from '../../../../component-library/components/BottomSheets/BottomSheet';
6-
import { strings } from '../../../../../locales/i18n';
7-
83
import {
4+
BottomSheet,
5+
type BottomSheetRef,
96
IconColor,
107
IconName,
118
IconSize,
12-
} from '../../../../component-library/components/Icons/Icon';
9+
} from '@metamask/design-system-react-native';
10+
import { strings } from '../../../../../locales/i18n';
1311
import ModalContent from '../../Notification/Modal';
1412
import { toggleBasicFunctionality } from '../../../../actions/settings';
1513
import { useParams } from '../../../../util/navigation/navUtils';
@@ -45,7 +43,7 @@ const ConfirmTurnOnBackupAndSyncModal = () => {
4543
const turnContent = {
4644
icon: {
4745
name: IconName.Check,
48-
color: IconColor.Success,
46+
color: IconColor.SuccessDefault,
4947
},
5048
bottomSheetTitle: strings('backupAndSync.enable.title'),
5149
bottomSheetMessage: strings('backupAndSync.enable.confirmation'),

app/components/UI/Notification/Modal/index.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import React from 'react';
22
import { View } from 'react-native';
33
import Checkbox from '../../../../component-library/components/Checkbox/Checkbox';
4-
import Icon, {
5-
IconColor,
6-
IconName,
7-
IconSize,
8-
} from '../../../../component-library/components/Icons/Icon';
9-
import Text, {
10-
TextVariant,
11-
} from '../../../../component-library/components/Texts/Text';
124
import {
135
Button,
146
ButtonVariant,
157
ButtonSize,
8+
Icon,
9+
IconColor,
10+
IconName,
11+
IconSize,
12+
Text,
13+
TextVariant,
1614
} from '@metamask/design-system-react-native';
1715
import createStyles from './styles';
1816
import { useTheme } from '../../../../util/theme';
@@ -60,10 +58,10 @@ const ModalContent = ({
6058
size={iconSize}
6159
style={styles.icon}
6260
/>
63-
<Text variant={TextVariant.HeadingMD} style={styles.title}>
61+
<Text variant={TextVariant.HeadingMd} style={styles.title}>
6462
{title}
6563
</Text>
66-
<Text variant={TextVariant.BodyMD} style={styles.description}>
64+
<Text variant={TextVariant.BodyMd} style={styles.description}>
6765
{message}
6866
</Text>
6967
<View style={styles.bottom}>

app/components/UI/TokenDetails/hooks/useTokenSecurityData.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,42 @@ describe('useTokenSecurityData', () => {
109109
expect(result.current.securityData).toBeNull();
110110
});
111111

112+
it('ignores prefetchedData with wrong shape and fetches instead', async () => {
113+
const assetId = 'eip155:1/erc20:0x1234' as CaipAssetType;
114+
mockFetchTokenAssets.mockResolvedValue([
115+
{
116+
assetId,
117+
name: 'Test Token',
118+
symbol: 'TEST',
119+
decimals: 18,
120+
securityData: mockSecurityData,
121+
},
122+
]);
123+
124+
// Bridge SecurityData shape: { type: "Verified" } — missing resultType
125+
const wrongShapedData = {
126+
type: 'Verified',
127+
} as unknown as TokenSecurityData;
128+
129+
const { result } = renderHook(() =>
130+
useTokenSecurityData({
131+
assetId,
132+
prefetchedData: wrongShapedData,
133+
}),
134+
);
135+
136+
expect(result.current.isLoading).toBe(true);
137+
138+
await waitFor(() => {
139+
expect(result.current.isLoading).toBe(false);
140+
});
141+
142+
expect(mockFetchTokenAssets).toHaveBeenCalledWith([assetId], {
143+
includeTokenSecurityData: true,
144+
});
145+
expect(result.current.securityData).toBe(mockSecurityData);
146+
});
147+
112148
it('does not fetch when assetId is null', () => {
113149
const { result } = renderHook(() =>
114150
useTokenSecurityData({ assetId: null }),

app/components/UI/TokenDetails/hooks/useTokenSecurityData.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,20 @@ interface UseTokenSecurityDataResult {
1818
error: Error | null;
1919
}
2020

21+
const isValidTokenSecurityData = (data: unknown): data is TokenSecurityData =>
22+
data != null &&
23+
typeof data === 'object' &&
24+
typeof (data as TokenSecurityData).resultType === 'string' &&
25+
Array.isArray((data as TokenSecurityData).features);
26+
2127
export const useTokenSecurityData = ({
2228
assetId,
23-
prefetchedData,
29+
prefetchedData: rawPrefetchedData,
2430
}: UseTokenSecurityDataOpts): UseTokenSecurityDataResult => {
31+
const prefetchedData = isValidTokenSecurityData(rawPrefetchedData)
32+
? rawPrefetchedData
33+
: undefined;
34+
2535
const [securityData, setSecurityData] = useState<TokenSecurityData | null>(
2636
prefetchedData ?? null,
2737
);

app/components/Views/SocialLeaderboard/TraderPositionView/TraderPositionView.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ jest.mock('../../../../core/ClipboardManager', () => ({
9292
setString: jest.fn().mockResolvedValue(undefined),
9393
}));
9494

95+
// Pressing buy mounts QuickBuyBottomSheet. Jest's global mock for design-system
96+
// `BottomSheet` (see app/util/test/testSetup.js) invokes `onOpenBottomSheet`'s
97+
// callback synchronously, so `QuickBuyBottomSheetContent` mounts in the same turn
98+
// and runs `useQuickBuyBottomSheet` (bridge selectors, device version compare,
99+
// NetworkController, …). This file intentionally uses a minimal Redux store, so
100+
// we stub the sheet here.
101+
jest.mock('./components/QuickBuyBottomSheet', () => ({
102+
__esModule: true,
103+
default: () => null,
104+
}));
105+
95106
jest.mock('../../../../util/haptics', () => {
96107
const actual = jest.requireActual<typeof import('../../../../util/haptics')>(
97108
'../../../../util/haptics',
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react-native';
3+
import { NavigationContainer } from '@react-navigation/native';
4+
5+
const mockNavigate = jest.fn();
6+
7+
jest.mock('@react-navigation/native', () => ({
8+
...jest.requireActual('@react-navigation/native'),
9+
useNavigation: () => ({ navigate: mockNavigate }),
10+
}));
11+
12+
jest.mock('react-redux', () => ({
13+
...jest.requireActual('react-redux'),
14+
useSelector: jest.fn(),
15+
}));
16+
17+
// Feed hooks — return empty/not-loading so NowTab renders without network calls.
18+
jest.mock('../feeds/tokens/useTokensFeed', () => ({
19+
useTokensFeed: jest.fn(() => ({ data: [], isLoading: false })),
20+
}));
21+
22+
jest.mock('../feeds/perps/usePerpsFeed', () => ({
23+
usePerpsFeed: jest.fn(() => ({ data: [], isLoading: false })),
24+
}));
25+
26+
jest.mock('../feeds/predictions/usePredictionsFeed', () => ({
27+
usePredictionsFeed: jest.fn(() => ({ data: [], isLoading: false })),
28+
}));
29+
30+
jest.mock('../feeds/stocks/useStocksFeed', () => ({
31+
useStocksFeed: jest.fn(() => ({ data: [], isLoading: false })),
32+
}));
33+
34+
// Mock PerpsSectionProvider as a transparent passthrough.
35+
jest.mock('../feeds/perps/PerpsSectionProvider', () => {
36+
// eslint-disable-next-line @typescript-eslint/no-require-imports
37+
const { createElement } = require('react');
38+
// eslint-disable-next-line @typescript-eslint/no-require-imports
39+
const { View } = require('react-native');
40+
return ({ children }: { children: unknown }) =>
41+
createElement(View, null, children);
42+
});
43+
44+
// Mock WhatsHappeningSection to keep its transitive deps (Engine, analytics)
45+
// out of this unit test. We control rendering via mockWhatsHappeningImpl.
46+
const mockWhatsHappeningImpl = jest.fn<React.ReactElement | null, [unknown]>(
47+
() => null,
48+
);
49+
50+
jest.mock('../../Homepage/Sections/WhatsHappening', () => {
51+
// eslint-disable-next-line @typescript-eslint/no-require-imports
52+
const { forwardRef } = require('react');
53+
return {
54+
__esModule: true,
55+
default: forwardRef((_props: unknown, ref: unknown) =>
56+
mockWhatsHappeningImpl(ref),
57+
),
58+
};
59+
});
60+
61+
import { useSelector } from 'react-redux';
62+
import { selectPerpsEnabledFlag } from '../../../UI/Perps';
63+
import { selectPredictEnabledFlag } from '../../../UI/Predict';
64+
import { selectWhatsHappeningEnabled } from '../../../../selectors/featureFlagController/whatsHappening';
65+
import NowTab from './NowTab';
66+
import type { RefreshConfig } from '../hooks/useExploreRefresh';
67+
68+
const defaultRefresh: RefreshConfig = { trigger: 0, silentRefresh: true };
69+
const defaultTabProps = {
70+
refresh: defaultRefresh,
71+
refreshing: false,
72+
onRefresh: jest.fn(),
73+
};
74+
75+
const renderNowTab = (props = defaultTabProps) =>
76+
render(
77+
<NavigationContainer>
78+
<NowTab {...props} />
79+
</NavigationContainer>,
80+
);
81+
82+
describe('NowTab — WhatsHappeningSection integration', () => {
83+
const mockUseSelector = useSelector as jest.MockedFunction<
84+
typeof useSelector
85+
>;
86+
87+
const mockSelectorBase = (selector: unknown) => {
88+
if (selector === selectPerpsEnabledFlag) return false;
89+
if (selector === selectPredictEnabledFlag) return false;
90+
return undefined;
91+
};
92+
93+
beforeEach(() => {
94+
jest.clearAllMocks();
95+
mockUseSelector.mockImplementation(mockSelectorBase);
96+
// Default: section mock renders nothing; individual tests override as needed.
97+
mockWhatsHappeningImpl.mockReturnValue(null);
98+
});
99+
100+
it('mounts WhatsHappeningSection and renders it when the feature flag is enabled', () => {
101+
mockUseSelector.mockImplementation((selector) => {
102+
if (selector === selectWhatsHappeningEnabled) return true;
103+
return mockSelectorBase(selector);
104+
});
105+
(mockWhatsHappeningImpl as jest.Mock).mockReturnValue(
106+
React.createElement('View', {
107+
testID: 'homepage-whats-happening-carousel',
108+
}),
109+
);
110+
111+
renderNowTab();
112+
113+
expect(
114+
screen.getByTestId('homepage-whats-happening-carousel'),
115+
).toBeOnTheScreen();
116+
});
117+
118+
it('does not mount WhatsHappeningSection when the feature flag is disabled', () => {
119+
mockUseSelector.mockImplementation((selector) => {
120+
if (selector === selectWhatsHappeningEnabled) return false;
121+
return mockSelectorBase(selector);
122+
});
123+
124+
renderNowTab();
125+
126+
// Section is not even mounted, so the mock should never have been called.
127+
expect(mockWhatsHappeningImpl).not.toHaveBeenCalled();
128+
expect(
129+
screen.queryByTestId('homepage-whats-happening-carousel'),
130+
).toBeNull();
131+
});
132+
133+
it('passes a ref to WhatsHappeningSection so pull-to-refresh can trigger it', () => {
134+
mockUseSelector.mockImplementation((selector) => {
135+
if (selector === selectWhatsHappeningEnabled) return true;
136+
return mockSelectorBase(selector);
137+
});
138+
139+
renderNowTab();
140+
141+
// The mock's first argument is the forwarded ref (we dropped props in the mock).
142+
// It should be a React ref object so the useEffect bridge can call .refresh().
143+
expect(mockWhatsHappeningImpl).toHaveBeenCalled();
144+
const [forwardedRef] = mockWhatsHappeningImpl.mock.calls[0];
145+
expect(forwardedRef).not.toBeNull();
146+
});
147+
});

0 commit comments

Comments
 (0)