Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const getStories = () => {
"./../../packages/design-system-react-native/src/components/AvatarIcon/AvatarIcon.stories.tsx": require("../../../packages/design-system-react-native/src/components/AvatarIcon/AvatarIcon.stories.tsx"),
"./../../packages/design-system-react-native/src/components/AvatarNetwork/AvatarNetwork.stories.tsx": require("../../../packages/design-system-react-native/src/components/AvatarNetwork/AvatarNetwork.stories.tsx"),
"./../../packages/design-system-react-native/src/components/AvatarToken/AvatarToken.stories.tsx": require("../../../packages/design-system-react-native/src/components/AvatarToken/AvatarToken.stories.tsx"),
"./../../packages/design-system-react-native/src/components/BadgeCount/BadgeCount.stories.tsx": require("../../../packages/design-system-react-native/src/components/BadgeCount/BadgeCount.stories.tsx"),
"./../../packages/design-system-react-native/src/components/BadgeNetwork/BadgeNetwork.stories.tsx": require("../../../packages/design-system-react-native/src/components/BadgeNetwork/BadgeNetwork.stories.tsx"),
"./../../packages/design-system-react-native/src/components/BadgeStatus/BadgeStatus.stories.tsx": require("../../../packages/design-system-react-native/src/components/BadgeStatus/BadgeStatus.stories.tsx"),
"./../../packages/design-system-react-native/src/components/Button/Button.stories.tsx": require("../../../packages/design-system-react-native/src/components/Button/Button.stories.tsx"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TextVariant } from '../Text';
import { BadgeCountSize } from './BadgeCount.types';

// Mappings
export const MAP_BADGECOUNT_SIZE_TEXTVARIANT: Record<
BadgeCountSize,
TextVariant
> = {
[BadgeCountSize.Md]: TextVariant.BodyXs,
[BadgeCountSize.Lg]: TextVariant.BodySm,
};
export const MAP_BADGECOUNT_SIZE_LINEHEIGHT: Record<BadgeCountSize, string> = {
[BadgeCountSize.Md]: 'leading-[14px]', // line-height 14px
[BadgeCountSize.Lg]: 'leading-4', // line-height 16px
};

export const TWCLASSMAP_BADGECOUNT_SIZE_CONTAINER: Record<
BadgeCountSize,
string
> = {
[BadgeCountSize.Md]: 'min-w-4 h-3.5 py-0 px-1', // min-width 16px, height 14px, padding-vertical 0, padding-horizontal 4
[BadgeCountSize.Lg]: 'min-w-6 h-5 py-0.5 px-1.5', // min-width 24px, height 20px, padding-vertical 2, padding-horizontal 6
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import { View } from 'react-native';

import BadgeCount from './BadgeCount';
import { BadgeCountSize } from './BadgeCount.types';
import type { BadgeCountProps } from './BadgeCount.types';

const meta: Meta<BadgeCountProps> = {
title: 'Components/BadgeCount',
component: BadgeCount,
argTypes: {
size: {
control: 'select',
options: BadgeCountSize,
},
count: {
control: 'number',
},
max: {
control: 'number',
},
twClassName: {
control: 'text',
},
},
};

export default meta;

type Story = StoryObj<BadgeCountProps>;

export const Default: Story = {
args: {
size: BadgeCountSize.Md,
count: 8,
max: 99,
twClassName: '',
},
};

export const Sizes: Story = {
render: () => (
<View style={{ gap: 8 }}>
{Object.values(BadgeCountSize).map((size) => (
<BadgeCount key={size} size={size} count={100} />
))}
</View>
),
};

export const Max: Story = {
render: () => (
<View style={{ gap: 16 }}>
<BadgeCount count={10} />
<BadgeCount count={100} />
</View>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { useTailwind } from '@metamask/design-system-twrnc-preset';

import Text, { TextColor, FontWeight } from '../Text';
import BadgeCount from './BadgeCount';
import { BadgeCountSize } from './BadgeCount.types';
import {
MAP_BADGECOUNT_SIZE_TEXTVARIANT,
TWCLASSMAP_BADGECOUNT_SIZE_CONTAINER,
MAP_BADGECOUNT_SIZE_LINEHEIGHT,
} from './BadgeCount.constants';

describe('BadgeCount', () => {
it('renders with default props and count less than max', () => {
const TestComponent = () => {
const tw = useTailwind();
const sizeVal = BadgeCountSize.Md;
const computedExpectedOuter = tw`
bg-error-default
rounded-lg
items-center
justify-center
self-start
${TWCLASSMAP_BADGECOUNT_SIZE_CONTAINER[sizeVal]}
`;
const expectedTextProps = {
variant: MAP_BADGECOUNT_SIZE_TEXTVARIANT[sizeVal],
color: TextColor.ErrorInverse,
fontWeight: FontWeight.Medium,
// Note: trailing space comes from template literal if textProps.twClassName is undefined.
twClassName: `${MAP_BADGECOUNT_SIZE_LINEHEIGHT[sizeVal]} `,
};
return (
<>
<BadgeCount count={50} testID="badge-count" />
<Text testID="expectedOuter">
{JSON.stringify(computedExpectedOuter)}
</Text>
<Text testID="expectedTextProps">
{JSON.stringify(expectedTextProps)}
</Text>
</>
);
};

const { getByTestId } = render(<TestComponent />);
const expectedOuter = JSON.parse(
getByTestId('expectedOuter').props.children,
);
const expectedTextProps = JSON.parse(
getByTestId('expectedTextProps').props.children,
);
const container = getByTestId('badge-count');
// Outer container style (first element of style array) should match expected
expect(container.props.style[0]).toStrictEqual(expectedOuter);
// Inner Text should render the count as a string
const textElement = container.props.children;
expect(textElement.props.children).toStrictEqual('50');
// Verify that Text props are set correctly
expect(textElement.props.variant).toStrictEqual(expectedTextProps.variant);
expect(textElement.props.color).toStrictEqual(expectedTextProps.color);
expect(textElement.props.fontWeight).toStrictEqual(
expectedTextProps.fontWeight,
);
expect(textElement.props.twClassName).toContain(
MAP_BADGECOUNT_SIZE_LINEHEIGHT[BadgeCountSize.Md],
);
});

it('renders with count greater than max (overflow)', () => {
const TestComponent = () => {
const tw = useTailwind();
const sizeVal = BadgeCountSize.Md;
const computedExpectedOuter = tw`
bg-error-default
rounded-lg
items-center
justify-center
self-start
${TWCLASSMAP_BADGECOUNT_SIZE_CONTAINER[sizeVal]}
`;
return (
<>
<BadgeCount count={150} max={99} testID="badge-count" />
<Text testID="expectedOuter">
{JSON.stringify(computedExpectedOuter)}
</Text>
</>
);
};

const { getByTestId } = render(<TestComponent />);
const expectedOuter = JSON.parse(
getByTestId('expectedOuter').props.children,
);
const container = getByTestId('badge-count');
expect(container.props.style[0]).toStrictEqual(expectedOuter);
const textElement = container.props.children;
// When count > max, text should be "99+"
expect(textElement.props.children).toStrictEqual('99+');
});

it('applies custom textProps overrides', () => {
const customTextProps = {
color: TextColor.PrimaryDefault,
fontWeight: FontWeight.Bold,
twClassName: 'custom',
};
const TestComponent = () => {
const tw = useTailwind();
const sizeVal = BadgeCountSize.Md;
const computedExpectedOuter = tw`
bg-error-default
rounded-lg
items-center
justify-center
self-start
${TWCLASSMAP_BADGECOUNT_SIZE_CONTAINER[sizeVal]}
`;
const expectedTextProps = {
variant: MAP_BADGECOUNT_SIZE_TEXTVARIANT[sizeVal],
color: customTextProps.color, // overridden
fontWeight: customTextProps.fontWeight, // overridden
twClassName: `${MAP_BADGECOUNT_SIZE_LINEHEIGHT[sizeVal]} ${customTextProps.twClassName}`,
};
return (
<>
<BadgeCount
count={25}
textProps={customTextProps}
testID="badge-count"
/>
<Text testID="expectedOuter">
{JSON.stringify(computedExpectedOuter)}
</Text>
<Text testID="expectedTextProps">
{JSON.stringify(expectedTextProps)}
</Text>
</>
);
};

const { getByTestId } = render(<TestComponent />);
const expectedOuter = JSON.parse(
getByTestId('expectedOuter').props.children,
);
const expectedTextProps = JSON.parse(
getByTestId('expectedTextProps').props.children,
);
const container = getByTestId('badge-count');
expect(container.props.style[0]).toStrictEqual(expectedOuter);
const textElement = container.props.children;
expect(textElement.props.children).toStrictEqual('25');
expect(textElement.props.variant).toStrictEqual(expectedTextProps.variant);
expect(textElement.props.color).toStrictEqual(expectedTextProps.color);
expect(textElement.props.fontWeight).toStrictEqual(
expectedTextProps.fontWeight,
);
expect(textElement.props.twClassName).toContain(
MAP_BADGECOUNT_SIZE_LINEHEIGHT[BadgeCountSize.Md],
);
expect(textElement.props.twClassName).toContain('custom');
});

it('applies additional container style and forwards extra props', () => {
const customStyle = { margin: 10 };
const extraProp = { accessibilityLabel: 'badge' };
const TestComponent = () => (
<BadgeCount
count={10}
style={customStyle}
testID="badge-count"
{...extraProp}
/>
);
const { getByTestId } = render(<TestComponent />);
const container = getByTestId('badge-count');
// The container style is an array; customStyle should be included.
expect(container.props.style).toEqual(
expect.arrayContaining([customStyle]),
);
expect(container.props.accessibilityLabel).toStrictEqual('badge');
});

it('renders with custom size Lg', () => {
const customSize = BadgeCountSize.Lg;
const TestComponent = () => {
const tw = useTailwind();
const computedExpectedOuter = tw`
bg-error-default
rounded-lg
items-center
justify-center
self-start
${TWCLASSMAP_BADGECOUNT_SIZE_CONTAINER[customSize]}
`;
const expectedTextProps = {
variant: MAP_BADGECOUNT_SIZE_TEXTVARIANT[customSize],
color: TextColor.ErrorInverse,
fontWeight: FontWeight.Medium,
twClassName: `${MAP_BADGECOUNT_SIZE_LINEHEIGHT[customSize]} `,
};
return (
<>
<BadgeCount count={5} size={customSize} testID="badge-count" />
<Text testID="expectedOuter">
{JSON.stringify(computedExpectedOuter)}
</Text>
<Text testID="expectedTextProps">
{JSON.stringify(expectedTextProps)}
</Text>
</>
);
};

const { getByTestId } = render(<TestComponent />);
const expectedOuter = JSON.parse(
getByTestId('expectedOuter').props.children,
);
const expectedTextProps = JSON.parse(
getByTestId('expectedTextProps').props.children,
);
const container = getByTestId('badge-count');
expect(container.props.style[0]).toStrictEqual(expectedOuter);
const textElement = container.props.children;
expect(textElement.props.variant).toStrictEqual(expectedTextProps.variant);
expect(textElement.props.twClassName).toContain(
MAP_BADGECOUNT_SIZE_LINEHEIGHT[customSize],
);
});
});
Copy link
Contributor

@georgewrmarshall georgewrmarshall Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your intent with ensuring we have good testing coverage here but I think these tests seem overly complex and test implementation details that make them brittle. For tests we should be focusing on:

  1. Core functionality:
    • count display
    • overflow behavior (max prop)
  2. Prop behavior:
    • textProps customization
    • style prop
    • size prop
    • prop forwarding

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import React from 'react';
import { View } from 'react-native';

import Text, { TextColor, FontWeight } from '../Text';
import {
MAP_BADGECOUNT_SIZE_TEXTVARIANT,
TWCLASSMAP_BADGECOUNT_SIZE_CONTAINER,
MAP_BADGECOUNT_SIZE_LINEHEIGHT,
} from './BadgeCount.constants';
import type { BadgeCountProps } from './BadgeCount.types';
import { BadgeCountSize } from './BadgeCount.types';

const BadgeCount = ({
size = BadgeCountSize.Md,
count,
max = 99,
textProps,
twClassName = '',
style,
...props
}: BadgeCountProps) => {
const tw = useTailwind();
const twContainerClassNames = `
bg-error-default
rounded-lg
items-center
justify-center
self-start
${TWCLASSMAP_BADGECOUNT_SIZE_CONTAINER[size]}
${twClassName}`;

return (
<View style={[tw`${twContainerClassNames}`, style]} {...props}>
<Text
variant={MAP_BADGECOUNT_SIZE_TEXTVARIANT[size as BadgeCountSize]}
color={TextColor.ErrorInverse}
fontWeight={FontWeight.Medium}
{...textProps}
twClassName={`${MAP_BADGECOUNT_SIZE_LINEHEIGHT[size]} ${textProps?.twClassName || ''}`}
>
{count > max ? `${max}+` : `${count}`}
</Text>
</View>
);
};

export default BadgeCount;
Loading
Loading