Skip to content

Commit 77a0c6c

Browse files
fix: lint violations maskicon (#680)
## **Description** This PR fixes linting violations in the Maskicon component across both React and React Native versions of the design system. The changes are focused on addressing various ESLint warnings and following best practices to ensure code consistency. Key improvements include: 1. Adopting proper test assertions using `toStrictEqual()` instead of `toEqual()` 2. Proper handling of asynchronous code with `void` 3. Following consistent import ordering conventions 4. Adding appropriate ESLint comments instead of disabling rules broadly 5. Improving type safety in the test mocks 6. Removing the Maskicon components from the ESLint ignore list now that violations are fixed ## **Related issues** Part of: - #657 - #658 ## **Manual testing steps** 1. Run `yarn lint` to verify linting violations are fixed 2. Run tests for the Maskicon component to ensure functionality still works: `yarn test Maskicon` 3. Build the component to ensure it still renders correctly: `yarn build` ## **Screenshots/Recordings** ### React Native <img width="48" alt="Screenshot 2025-05-22 at 2 06 16 PM" src="https://github.com/user-attachments/assets/666fa27e-127b-4f55-bba7-47c7759aafd1" /><img width="58" alt="Screenshot 2025-05-22 at 2 08 43 PM" src="https://github.com/user-attachments/assets/89fc7294-722d-4a59-b989-0e23c10e9fa0" /> ### React <img width="446" alt="Screenshot 2025-05-22 at 2 06 46 PM" src="https://github.com/user-attachments/assets/de17fe24-bdca-4510-a0bc-983c68271201" /> <img width="455" alt="Screenshot 2025-05-22 at 2 08 30 PM" src="https://github.com/user-attachments/assets/25e93ca2-7b52-4fb8-b598-dddad320eb48" /> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) - [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-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors.
1 parent 8415efa commit 77a0c6c

File tree

8 files changed

+184
-96
lines changed

8 files changed

+184
-96
lines changed

eslint.config.mjs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ const config = createConfig([
2626
'packages/design-system-react/src/components/BadgeWrapper/BadgeWrapper.test.tsx',
2727
'packages/design-system-react/src/components/temp-components/Jazzicon/Jazzicon.test.tsx',
2828
'packages/design-system-react/src/components/temp-components/Jazzicon/Jazzicon.tsx',
29-
'packages/design-system-react/src/components/temp-components/Maskicon/Maskicon.test.tsx',
30-
'packages/design-system-react/src/components/temp-components/Maskicon/Maskicon.tsx',
31-
'packages/design-system-react/src/components/temp-components/Maskicon/Maskicon.utilities.ts',
3229
// design system react native
3330
'packages/design-system-react-native/metro.config.js',
3431
'packages/design-system-react-native/jest.setup.js',
@@ -61,9 +58,6 @@ const config = createConfig([
6158
'packages/design-system-react-native/src/components/temp-components/ImageOrSvg/ImageOrSvg.stories.tsx',
6259
'packages/design-system-react-native/src/components/temp-components/ImageOrSvg/ImageOrSvg.test.tsx',
6360
'packages/design-system-react-native/src/components/temp-components/ImageOrSvg/ImageOrSvg.tsx',
64-
'packages/design-system-react-native/src/components/temp-components/Maskicon/Maskicon.test.tsx',
65-
'packages/design-system-react-native/src/components/temp-components/Maskicon/Maskicon.tsx',
66-
'packages/design-system-react-native/src/components/temp-components/Maskicon/Maskicon.utilities.ts',
6761
'packages/design-system-react-native/src/components/temp-components/Spinner/Spinner.tsx',
6862
// storybook react
6963
'apps/storybook-react/.storybook/*.ts',

packages/design-system-react-native/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ module.exports = merge(baseConfig, {
3131
'!**/*.dev.{js,ts}', // Exclude .dev files
3232
'!**/*.assets.{js,ts}', // Exclude .assets files
3333
'!**/*.types.{js,ts}', // Exclude .types files
34+
'!./src/components/temp-components/Blockies/Blockies.utilities.d.ts',
3435
],
3536
// Add coverage ignore patterns
3637
coveragePathIgnorePatterns: [

packages/design-system-react-native/src/components/temp-components/Maskicon/Maskicon.test.tsx

Lines changed: 59 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import React from 'react';
2-
import { render, waitFor } from '@testing-library/react-native';
31
import { KnownCaipNamespace, stringToBytes } from '@metamask/utils';
2+
import { render, waitFor } from '@testing-library/react-native';
3+
import React from 'react';
44

55
import { Maskicon } from './Maskicon';
66
import * as MaskiconUtilities from './Maskicon.utilities';
77

88
jest.mock('bitcoin-address-validation', () => ({
9-
validate: (address: string, network: any) => {
10-
if (address === '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa') return true;
9+
validate: (address: string, _network: unknown) => {
10+
if (address === '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa') {
11+
return true;
12+
}
1113
return false;
1214
},
1315
Network: {
@@ -20,107 +22,125 @@ jest.mock('@solana/addresses', () => ({
2022
isAddress: (address: string) => address === 'ValidSolanaAddress',
2123
}));
2224

25+
// Polyfill TextEncoder for JSDOM (Node < 18)
26+
if (typeof TextEncoder === 'undefined') {
27+
// eslint-disable-next-line import-x/no-nodejs-modules, @typescript-eslint/no-require-imports
28+
global.TextEncoder = require('util').TextEncoder;
29+
}
30+
2331
// Stub for react-native-svg so the component renders without error.
2432
jest.mock('react-native-svg', () => {
25-
const React = require('react');
2633
return {
27-
SvgXml: (props: any) => React.createElement('SvgXml', props, props.xml),
34+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35+
SvgXml: (props: any) => (
36+
<div
37+
data-testid={props.testID}
38+
style={{ width: props.width, height: props.height }}
39+
width={props.width}
40+
height={props.height}
41+
xml={props.xml}
42+
{...props}
43+
/>
44+
),
2845
};
2946
});
3047

3148
// A simple deferred promise helper to control when a Promise resolves.
3249
const createDeferred = <T,>() => {
33-
let resolve: (value: T) => void;
34-
let reject: (error: any) => void;
35-
const promise = new Promise<T>((res, rej) => {
36-
resolve = res;
37-
reject = rej;
50+
let resolver: (value: T) => void;
51+
let rejector: (error: unknown) => void;
52+
const promise = new Promise<T>((resolve, reject) => {
53+
resolver = resolve;
54+
rejector = reject;
3855
});
39-
return { promise, resolve: resolve!, reject: reject! };
56+
57+
// Using non-null assertion is safe here because we know resolver and rejector are assigned in the Promise constructor
58+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
59+
return { promise, resolve: resolver!, reject: rejector! };
4060
};
4161

4262
describe('Maskicon Utilities', () => {
43-
test('generateSeedEthereum returns numeric seed based on address slice', () => {
63+
it('generateSeedEthereum returns numeric seed based on address slice', () => {
4464
const address = '0x9Cbf7c41B7787F6c621115010D3B044029FE2Ce8';
4565
const expectedSeed = parseInt(address.slice(2, 10), 16);
4666
expect(MaskiconUtilities.generateSeedEthereum(address)).toBe(expectedSeed);
4767
});
4868

49-
test('generateSeedNonEthereum returns byte-array seed from normalized lowercased address', () => {
69+
it('generateSeedNonEthereum returns byte-array seed from normalized lowercased address', () => {
5070
const address = 'TestAddress';
5171
const normalized = address.normalize('NFKC').toLowerCase();
5272
const expectedSeed = Array.from(stringToBytes(normalized));
53-
expect(MaskiconUtilities.generateSeedNonEthereum(address)).toEqual(
73+
expect(MaskiconUtilities.generateSeedNonEthereum(address)).toStrictEqual(
5474
expectedSeed,
5575
);
5676
});
5777

5878
describe('seedToString helper', () => {
59-
test('pads a numeric seed if hex is less than 6 characters', () => {
79+
it('pads a numeric seed if hex is less than 6 characters', () => {
6080
// For example, 1 in hex is "1", so it should be padded to "100000".
6181
const result = MaskiconUtilities.seedToString(1);
6282
expect(result).toBe('100000');
6383
});
6484

65-
test('converts a byte array seed to hex and pads if necessary', () => {
85+
it('converts a byte array seed to hex and pads if necessary', () => {
6686
// For an array like [1] which converts to "01", it is padded to "010000".
6787
const result = MaskiconUtilities.seedToString([1]);
6888
expect(result).toBe('010000');
6989
});
7090

71-
test('returns "seed000" for unsupported seed types', () => {
72-
// When provided seed is not a number or an array.
91+
it('returns "seed000" for unsupported seed types', () => {
92+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7393
const result = MaskiconUtilities.seedToString({} as any);
7494
expect(result).toBe('seed000');
7595
});
7696
});
7797

7898
describe('getCaipNamespaceFromAddress', () => {
79-
test('returns Eip155 when address starts with "0x"', async () => {
99+
it('returns Eip155 when address starts with "0x"', async () => {
80100
const address = '0xabcdef1234567890abcdef1234567890abcdef12';
81101
const ns = await MaskiconUtilities.getCaipNamespaceFromAddress(address);
82102
expect(ns).toBe(KnownCaipNamespace.Eip155);
83103
});
84104

85-
test('returns Bip122 for CAIP-10 formatted address "bip122:..."', async () => {
105+
it('returns Bip122 for CAIP-10 formatted address "bip122:..."', async () => {
86106
const address = 'bip122:someAddress';
87107
const ns = await MaskiconUtilities.getCaipNamespaceFromAddress(address);
88108
expect(ns).toBe(KnownCaipNamespace.Bip122);
89109
});
90110

91-
test('returns Solana for CAIP-10 formatted address "solana:..."', async () => {
111+
it('returns Solana for CAIP-10 formatted address "solana:..."', async () => {
92112
const address = 'solana:someAddress';
93113
const ns = await MaskiconUtilities.getCaipNamespaceFromAddress(address);
94114
expect(ns).toBe(KnownCaipNamespace.Solana);
95115
});
96116

97-
test('returns Bip122 for valid Bitcoin address (dynamic import branch)', async () => {
117+
it('returns Bip122 for valid Bitcoin address (dynamic import branch)', async () => {
98118
const address = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa';
99119
const ns = await MaskiconUtilities.getCaipNamespaceFromAddress(address);
100120
expect(ns).toBe(KnownCaipNamespace.Bip122);
101121
});
102122

103-
test('returns Solana for valid Solana address (fallback branch)', async () => {
123+
it('returns Solana for valid Solana address (fallback branch)', async () => {
104124
const address = 'ValidSolanaAddress';
105125
const ns = await MaskiconUtilities.getCaipNamespaceFromAddress(address);
106126
expect(ns).toBe(KnownCaipNamespace.Solana);
107127
});
108128

109-
test('returns Eip155 for CAIP-10 formatted address with mixed-case namespace "Eip155:someAddress"', async () => {
129+
it('returns Eip155 for CAIP-10 formatted address with mixed-case namespace "Eip155:someAddress"', async () => {
110130
const address = 'Eip155:someAddress';
111131
const ns = await MaskiconUtilities.getCaipNamespaceFromAddress(address);
112132
expect(ns).toBe(KnownCaipNamespace.Eip155);
113133
});
114134

115-
test('returns Eip155 when none of the conditions match (fallback)', async () => {
135+
it('returns Eip155 when none of the conditions match (fallback)', async () => {
116136
const address = 'nonEthereumNonSolanaAddress';
117137
const ns = await MaskiconUtilities.getCaipNamespaceFromAddress(address);
118138
expect(ns).toBe(KnownCaipNamespace.Eip155);
119139
});
120140
});
121141

122142
describe('createMaskiconSVG', () => {
123-
test('generates an SVG string using numeric seed', () => {
143+
it('generates an SVG string using numeric seed', () => {
124144
const seed = 123456;
125145
const size = 100;
126146
const svg = MaskiconUtilities.createMaskiconSVG(seed, size);
@@ -131,7 +151,7 @@ describe('Maskicon Utilities', () => {
131151
expect(svg).toContain('<path');
132152
});
133153

134-
test('generates an SVG string using array seed', () => {
154+
it('generates an SVG string using array seed', () => {
135155
const seed = [1, 2, 3, 4, 5];
136156
const size = 50;
137157
const svg = MaskiconUtilities.createMaskiconSVG(seed, size);
@@ -142,14 +162,14 @@ describe('Maskicon Utilities', () => {
142162
expect(svg).toContain('<path');
143163
});
144164

145-
test('uses default size 100 if size is not provided', () => {
165+
it('uses default size 100 if size is not provided', () => {
146166
const seed = 123456;
147167
const svg = MaskiconUtilities.createMaskiconSVG(seed);
148168
expect(svg).toContain('width="100"');
149169
expect(svg).toContain('height="100"');
150170
});
151171

152-
test('triangle branch (rotation 270) produces expected path segment', () => {
172+
it('triangle branch (rotation 270) produces expected path segment', () => {
153173
const hashSpy = jest
154174
.spyOn(MaskiconUtilities, 'sdbmHash')
155175
.mockReturnValue(768);
@@ -167,36 +187,34 @@ describe('Maskicon Utilities', () => {
167187
});
168188

169189
describe('getMaskiconSVG caching and non-Ethereum branch', () => {
170-
test('getMaskiconSVG returns consistent SVG and uses caching', async () => {
190+
it('getMaskiconSVG returns consistent SVG and uses caching', async () => {
171191
const address = '0x9Cbf7c41B7787F6c621115010D3B044029FE2Ce8';
172192
const size = 100;
173193
const svg1 = await MaskiconUtilities.getMaskiconSVG(address, size);
174194
const svg2 = await MaskiconUtilities.getMaskiconSVG(address, size);
175-
expect(svg1).toEqual(svg2);
195+
expect(svg1).toStrictEqual(svg2);
176196
});
177197

178-
test('uses generateSeedNonEthereum when namespace is not Eip155', async () => {
198+
it('uses generateSeedNonEthereum when namespace is not Eip155', async () => {
179199
// Use a CAIP-formatted address that forces a non-Ethereum (e.g. Solana) branch.
180200
const addressNonEth = 'solana:someAddress';
181201
const size = 100;
182202
const svgNonEth = await MaskiconUtilities.getMaskiconSVG(
183203
addressNonEth,
184204
size,
185205
);
186-
187206
// For comparison, generate an Ethereum version.
188207
const ethAddress = '0xABCDEF1234567890ABCDEF1234567890ABCDEF12';
189208
const svgEth = await MaskiconUtilities.getMaskiconSVG(ethAddress, size);
190-
191209
// They should be different, indicating the non-Ethereum branch (using generateSeedNonEthereum) was taken.
192-
expect(svgNonEth).not.toEqual(svgEth);
193-
expect(svgNonEth).toContain('<svg');
210+
expect(svgNonEth).not.toStrictEqual(svgEth);
211+
expect(svgNonEth).toStrictEqual(expect.stringContaining('<svg'));
194212
});
195213
});
196214
});
197215

198216
describe('Maskicon Component', () => {
199-
test('defaults size prop to 32 if size is not provided', async () => {
217+
it('defaults size prop to 32 if size is not provided', async () => {
200218
const { getByTestId } = render(
201219
<Maskicon
202220
address="0x9Cbf7c41B7787F6c621115010D3B044029FE2Ce8"
@@ -211,7 +229,7 @@ describe('Maskicon Component', () => {
211229
expect(svgElement.props.height).toBe(32);
212230
});
213231

214-
test('renders SvgXml with correct properties once SVG is ready', async () => {
232+
it('renders SvgXml with correct properties once SVG is ready', async () => {
215233
const { getByTestId } = render(
216234
<Maskicon
217235
address="0x9Cbf7c41B7787F6c621115010D3B044029FE2Ce8"
@@ -225,7 +243,7 @@ describe('Maskicon Component', () => {
225243
expect(svgElement.props.xml).toContain('<svg');
226244
});
227245

228-
test('forwards additional props to the SvgXml component', async () => {
246+
it('forwards additional props to the SvgXml component', async () => {
229247
const { getByTestId } = render(
230248
<Maskicon
231249
address="0x9Cbf7c41B7787F6c621115010D3B044029FE2Ce8"
@@ -239,7 +257,7 @@ describe('Maskicon Component', () => {
239257
expect(forwardedElement).toBeDefined();
240258
});
241259

242-
test('does not update state if component unmounts before the async effect resolves', async () => {
260+
it('does not update state if component unmounts before the async effect resolves', async () => {
243261
const deferred = createDeferred<string>();
244262

245263
const spy = jest

packages/design-system-react-native/src/components/temp-components/Maskicon/Maskicon.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
21
import React, { useEffect, useState } from 'react';
32
import { View } from 'react-native';
43
import { SvgXml } from 'react-native-svg';
54

6-
import { getMaskiconSVG } from './Maskicon.utilities';
75
import type { MaskiconProps } from './Maskicon.types';
6+
import { getMaskiconSVG } from './Maskicon.utilities';
87

98
export const Maskicon = ({ address, size = 32, ...props }: MaskiconProps) => {
109
const [svgString, setSvgString] = useState('');
1110

1211
useEffect(() => {
1312
let cancelled = false;
14-
(async () => {
13+
// eslint-disable-next-line no-void
14+
void (async () => {
1515
const newSvg = await getMaskiconSVG(address, size);
1616
if (!cancelled) {
1717
setSvgString(newSvg);
@@ -22,8 +22,8 @@ export const Maskicon = ({ address, size = 32, ...props }: MaskiconProps) => {
2222
};
2323
}, [address, size]);
2424

25-
if (!svgString || typeof SvgXml !== 'function') {
26-
return <View style={[{ width: size, height: size }]} />;
25+
if (!svgString) {
26+
return <View style={{ width: size, height: size }} />;
2727
}
2828

2929
return <SvgXml xml={svgString} width={size} height={size} {...props} />;

0 commit comments

Comments
 (0)