Skip to content

Commit 88e375e

Browse files
test: color-no-hex assets batch (#27150)
## **Description** Split from #26651 to reduce CODEOWNERS fanout. Batch: assets Source branch: \`origin/remove-static-hex-from-tests\` When \`@metamask/design-tokens\` upgrades change color values, tests that hardcode hex literals (e.g. \`'#ffffff'\`, \`'#457a39'\`) break because the component renders the new token value while the test still asserts the old one — even though nothing is actually wrong. This PR fixes that brittleness across the assets scope. **Two strategies are applied:** 1. **Replace hardcoded hex theme mocks with \`mockTheme\`** — test mocks that hand-rolled partial theme objects (e.g. \`{ colors: { primary: { default: '#0376C9' } } }\`) are replaced with \`jest.requireActual\` to pull the real \`mockTheme\`. Test assertions that checked against hex literals (e.g. \`expect(color).toBe('#457a39')\`) now reference \`mockTheme.colors.success.default\`, so both the component and the test always resolve the same value regardless of token package version. 2. **Add targeted \`eslint-disable\` comments** — strings like \`'#113'\` or \`'#6904'\` are NFT token IDs, not CSS colors. The \`color-no-hex\` rule can't distinguish them, so inline suppressions are added to allow these legitimate domain strings without weakening the rule elsewhere. Together these changes make the \`@metamask/design-tokens/color-no-hex\` lint rule enforceable across this scope, so future hex literals won't accidentally creep back in and cause brittle tests. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** \`\`\`gherkin Feature: color-no-hex lint compliance (assets batch) Scenario: user runs lint and tests Given the asset test files have been updated When user runs lint and test checks Then no color-no-hex violations are reported And all tests pass \`\`\` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: changes are confined to ESLint configuration and test mocks/assertions to avoid hardcoded hex values, with no runtime logic impact. > > **Overview** > **Enables `@metamask/design-tokens/color-no-hex` enforcement for the Assets-owned UI/hooks surface** by expanding the ESLint override to treat hex literals as errors in those directories. > > Updates multiple Assets-related tests to remove hardcoded hex color mocks and assertions by sourcing colors from the real `mockTheme` via `jest.requireActual`, and adds targeted inline disables where strings like `#113`/`#6904` are NFT identifiers (not color literals). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 02aaba1. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent ba36624 commit 88e375e

10 files changed

Lines changed: 107 additions & 75 deletions

File tree

.eslintrc.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ module.exports = {
181181
},
182182
{
183183
files: [
184+
'app/components/hooks/useIsOriginalNativeTokenSymbol/**/*.{js,jsx,ts,tsx}',
185+
'app/components/hooks/useTokenBalancesController/**/*.{js,jsx,ts,tsx}',
186+
'app/components/hooks/useTokenBalance.tsx',
187+
'app/components/hooks/useTokensData/**/*.{js,jsx,ts,tsx}',
188+
'app/components/hooks/useSafeChains.ts',
184189
'app/components/UI/Card/**/*.{js,jsx,ts,tsx}',
185190
'app/components/Snaps/**/*.{js,jsx,ts,tsx}',
186191
'app/components/UI/Predict/**/*.{js,jsx,ts,tsx}',
@@ -189,6 +194,32 @@ module.exports = {
189194
'app/components/UI/Perps/**/*.{js,jsx,ts,tsx}',
190195
'app/components/UI/Earn/**/*.{js,jsx,ts,tsx}',
191196
'app/components/UI/Stake/**/*.{js,jsx,ts,tsx}',
197+
// Assets team has a large number of folder ownership areas,
198+
'app/components/UI/Assets/**/*.{js,jsx,ts,tsx}',
199+
'app/components/UI/Tokens/**/*.{js,jsx,ts,tsx}',
200+
'app/components/UI/AssetOverview/**/*.{js,jsx,ts,tsx}',
201+
'app/components/UI/Collectibles/**/*.{js,jsx,ts,tsx}',
202+
'app/components/UI/CollectibleContractElement/**/*.{js,jsx,ts,tsx}',
203+
'app/components/UI/CollectibleContractInformation/**/*.{js,jsx,ts,tsx}',
204+
'app/components/UI/CollectibleContractOverview/**/*.{js,jsx,ts,tsx}',
205+
'app/components/UI/CollectibleContracts/**/*.{js,jsx,ts,tsx}',
206+
'app/components/UI/CollectibleDetectionModal/**/*.{js,jsx,ts,tsx}',
207+
'app/components/UI/CollectibleMedia/**/*.{js,jsx,ts,tsx}',
208+
'app/components/UI/CollectibleModal/**/*.{js,jsx,ts,tsx}',
209+
'app/components/UI/CollectibleOverview/**/*.{js,jsx,ts,tsx}',
210+
'app/components/UI/ConfirmAddAsset/**/*.{js,jsx,ts,tsx}',
211+
'app/components/UI/DeFiPositions/**/*.{js,jsx,ts,tsx}',
212+
'app/components/UI/TokenDetails/**/*.{js,jsx,ts,tsx}',
213+
'app/components/Views/AddAsset/**/*.{js,jsx,ts,tsx}',
214+
'app/components/Views/Asset/**/*.{js,jsx,ts,tsx}',
215+
'app/components/Views/AssetDetails/**/*.{js,jsx,ts,tsx}',
216+
'app/components/Views/AssetHideConfirmation/**/*.{js,jsx,ts,tsx}',
217+
'app/components/Views/AssetOptions/**/*.{js,jsx,ts,tsx}',
218+
'app/components/Views/Collectible/**/*.{js,jsx,ts,tsx}',
219+
'app/components/Views/CollectibleView/**/*.{js,jsx,ts,tsx}',
220+
'app/components/Views/DetectedTokens/**/*.{js,jsx,ts,tsx}',
221+
'app/components/Views/NFTAutoDetectionModal/**/*.{js,jsx,ts,tsx}',
222+
'app/components/Views/NftDetails/**/*.{js,jsx,ts,tsx}',
192223
],
193224
rules: {
194225
'@metamask/design-tokens/color-no-hex': 'error',

app/components/UI/AssetOverview/TronEnergyBandwidthDetail/ResourceRing.test.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@ jest.mock('@metamask/design-system-twrnc-preset', () => ({
1010
},
1111
}));
1212

13-
const mockTheme = {
14-
colors: {
15-
border: { muted: '#CCCCCC' },
16-
primary: { default: '#0066CC' },
17-
},
18-
};
19-
jest.mock('../../../../util/theme', () => ({
20-
useTheme: () => mockTheme,
21-
}));
13+
jest.mock('../../../../util/theme', () => {
14+
// Use the real mockTheme to avoid circular mock issues
15+
const actual = jest.requireActual('../../../../util/theme');
16+
return {
17+
...actual,
18+
useTheme: () => actual.mockTheme,
19+
};
20+
});
2221

2322
describe('ResourceRing', () => {
2423
it('renders the ring icon', () => {

app/components/UI/CollectibleContractOverview/index.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const initialState = {
4949
favorite: false,
5050
image: 'https://image.com/113',
5151
isCurrentlyOwned: true,
52+
// eslint-disable-next-line @metamask/design-tokens/color-no-hex -- false positive: '#113' is part of the NFT name, not a color literal
5253
name: 'My Nft #113',
5354
standard: 'ERC721',
5455
tokenId: '113',
@@ -108,6 +109,7 @@ describe('CollectibleContractOverview', () => {
108109
favorite: false,
109110
image: 'https://image.com/113',
110111
isCurrentlyOwned: true,
112+
// eslint-disable-next-line @metamask/design-tokens/color-no-hex -- false positive: '#113' is part of the NFT name, not a color literal
111113
name: 'My Nft #113',
112114
standard: 'ERC721',
113115
tokenId: '113',

app/components/UI/CollectibleModal/CollectibleModal.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ describe('CollectibleModal', () => {
114114
state: mockInitialState,
115115
});
116116

117+
// eslint-disable-next-line @metamask/design-tokens/color-no-hex -- false positive: '#6904' is the NFT token ID text, not a color literal
117118
expect(await findAllByText('#6904')).toBeDefined();
118119
expect(await findAllByText('Leopard')).toBeDefined();
119120
});

app/components/UI/Tokens/TokenList/ScamWarningModal/ScamWarningModal.test.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@ jest.mock('@react-navigation/native', () => ({
1313
useNavigation: jest.fn(),
1414
}));
1515

16-
jest.mock('../../../../../util/theme', () => ({
17-
useTheme: () => ({
18-
colors: {
19-
background: { default: '#fff' },
20-
border: { muted: '#ccc' },
21-
overlay: { default: 'rgba(0,0,0,0.5)' },
22-
},
23-
}),
24-
}));
16+
jest.mock('../../../../../util/theme', () => {
17+
// Use the real mockTheme to avoid hardcoded hex literals
18+
const actual = jest.requireActual('../../../../../util/theme');
19+
return {
20+
...actual,
21+
useTheme: () => actual.mockTheme,
22+
};
23+
});
2524

2625
jest.mock(
2726
'../../../../../component-library/components/Sheet/SheetHeader',

app/components/UI/Tokens/TokenList/TokenList.test.tsx

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { useNavigation } from '@react-navigation/native';
88
import { WalletViewSelectorsIDs } from '../../../Views/Wallet/WalletView.testIds';
99
import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
1010
import { AnalyticsEventBuilder } from '../../../../util/analytics/AnalyticsEventBuilder';
11-
import { createMockUseAnalyticsHook } from '../../../../util/test/analyticsMock';
1211
import { SCROLL_TO_TOKEN_EVENT } from '../constants';
1312

1413
// Mock external dependencies
@@ -18,14 +17,12 @@ jest.mock('@react-navigation/native', () => ({
1817

1918
jest.mock('../../../hooks/useAnalytics/useAnalytics');
2019

21-
jest.mock('../../../../util/theme', () => ({
22-
useTheme: () => ({
23-
colors: {
24-
primary: { default: '#0376C9' },
25-
icon: { default: '#24292E' },
26-
},
27-
}),
28-
}));
20+
jest.mock('../../../../util/theme', () => {
21+
const { mockTheme } = jest.requireActual('../../../../util/theme');
22+
return {
23+
useTheme: () => mockTheme,
24+
};
25+
});
2926

3027
jest.mock('../../../../../locales/i18n', () => ({
3128
strings: jest.fn((key) => key),
@@ -143,7 +140,9 @@ const mockUseNavigation = useNavigation as jest.MockedFunction<
143140
typeof useNavigation
144141
>;
145142
const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
146-
const mockUseAnalytics = jest.mocked(useAnalytics);
143+
const mockUseAnalytics = useAnalytics as jest.MockedFunction<
144+
typeof useAnalytics
145+
>;
147146

148147
const mockTokenKeys = [
149148
{
@@ -175,14 +174,20 @@ describe('TokenList', () => {
175174
navigate: mockNavigate,
176175
} as unknown as ReturnType<typeof useNavigation>);
177176

178-
mockUseAnalytics.mockReturnValue(
179-
createMockUseAnalyticsHook({
180-
trackEvent: mockTrackEvent,
181-
// The real builder is needed so build() produces the correct event shape
182-
// for assertions on trackEvent call arguments.
183-
createEventBuilder: AnalyticsEventBuilder.createEventBuilder,
184-
}),
185-
);
177+
mockUseAnalytics.mockReturnValue({
178+
trackEvent: mockTrackEvent,
179+
createEventBuilder: AnalyticsEventBuilder.createEventBuilder,
180+
enable: jest.fn(),
181+
addTraitsToUser: jest.fn(),
182+
createDataDeletionTask: jest.fn(),
183+
checkDataDeleteStatus: jest.fn(),
184+
getDeleteRegulationCreationDate: jest.fn(),
185+
getDeleteRegulationId: jest.fn(),
186+
isDataRecorded: jest.fn(),
187+
isEnabled: jest.fn(),
188+
getAnalyticsId: jest.fn(),
189+
identify: jest.fn(),
190+
});
186191

187192
// Mock useSelector to call the selector function with empty state
188193
mockUseSelector.mockImplementation((selector) => selector({}));

app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.test.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BtcAccountType } from '@metamask/keyring-api';
22
import React from 'react';
33
import { useSelector } from 'react-redux';
4+
import { mockTheme } from '../../../../../util/theme';
45
import { ACCOUNT_TYPE_LABEL_TEST_ID, TokenListItem } from './TokenListItem';
56
import { FlashListAssetKey } from '../TokenList';
67
import { useTokenPricePercentageChange } from '../../hooks/useTokenPricePercentageChange';
@@ -72,9 +73,15 @@ jest.mock('@react-navigation/native', () => ({
7273
}),
7374
}));
7475

75-
jest.mock('../../../../../util/theme', () => ({
76-
useTheme: () => ({ colors: {} }),
77-
}));
76+
jest.mock('../../../../../util/theme', () => {
77+
const { mockTheme: realMockTheme } = jest.requireActual(
78+
'../../../../../util/theme',
79+
);
80+
return {
81+
useTheme: () => ({ colors: {} }),
82+
mockTheme: realMockTheme,
83+
};
84+
});
7885

7986
const FIXED_NOW_MS = 1730000000000;
8087
const mockTrackEvent = jest.fn();
@@ -547,7 +554,9 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => {
547554
const percentageText = getByTestId(SECONDARY_BALANCE_TEST_ID);
548555

549556
expect(percentageText.props.children).toBe('+5.67%');
550-
expect(percentageText.props.style.color).toBe('#457a39');
557+
expect(percentageText.props.style.color).toBe(
558+
mockTheme.colors.success.default,
559+
);
551560
});
552561

553562
it('displays dash when percentage change is not finite', () => {
@@ -601,8 +610,9 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => {
601610

602611
const percentageText = getByTestId(SECONDARY_BALANCE_TEST_ID);
603612
expect(percentageText.props.children).toBe('-3.45%');
604-
// Negative percentage should NOT have success color
605-
expect(percentageText.props.style.color).not.toBe('#457a39');
613+
expect(percentageText.props.style.color).toBe(
614+
mockTheme.colors.error.default,
615+
);
606616
});
607617

608618
it('hides percentage change on testnet', () => {

app/components/UI/Tokens/TokenList/TokenListSkeleton/TokenListSkeleton.test.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,12 @@ import { render } from '@testing-library/react-native';
33
import TokenListSkeleton from './TokenListSkeleton';
44

55
// Mock the theme hook
6-
jest.mock('../../../../../util/theme', () => ({
7-
useTheme: () => ({
8-
colors: {
9-
background: {
10-
section: '#E5E5E5',
11-
subsection: '#F5F5F5',
12-
default: '#FFFFFF',
13-
},
14-
border: {
15-
muted: '#D6D9DC',
16-
},
17-
},
18-
}),
19-
}));
6+
jest.mock('../../../../../util/theme', () => {
7+
const { mockTheme } = jest.requireActual('../../../../../util/theme');
8+
return {
9+
useTheme: () => mockTheme,
10+
};
11+
});
2012

2113
describe('TokenListSkeleton', () => {
2214
it('renders without errors', () => {

app/components/UI/Tokens/TokenListControlBar/TokenListControlBar.test.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,12 @@ jest.mock('../../../../selectors/networkInfos', () => ({
9494
}));
9595

9696
// Mock the theme
97-
jest.mock('../../../../util/theme', () => ({
98-
useTheme: () => ({
99-
colors: {
100-
background: { default: '#ffffff' },
101-
text: { default: '#000000' },
102-
border: { muted: '#e0e0e0' },
103-
},
104-
}),
105-
}));
97+
jest.mock('../../../../util/theme', () => {
98+
const { mockTheme } = jest.requireActual('../../../../util/theme');
99+
return {
100+
useTheme: () => mockTheme,
101+
};
102+
});
106103

107104
const mockStore = configureMockStore();
108105

app/components/Views/DetectedTokens/index.test.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,12 @@ jest.mock('@react-navigation/native', () => ({
2727
})),
2828
}));
2929

30-
jest.mock('../../../util/theme', () => ({
31-
useTheme: jest.fn(() => ({
32-
colors: {
33-
background: { default: '#fff' },
34-
border: { default: '#ccc' },
35-
text: { default: '#000' },
36-
primary: { default: '#f00' },
37-
},
38-
})),
39-
}));
30+
jest.mock('../../../util/theme', () => {
31+
const { mockTheme } = jest.requireActual('../../../util/theme');
32+
return {
33+
useTheme: jest.fn(() => mockTheme),
34+
};
35+
});
4036

4137
jest.mock(
4238
'../../../component-library/components/BottomSheets/BottomSheet',

0 commit comments

Comments
 (0)