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
126 changes: 98 additions & 28 deletions app/components/UI/Rewards/Views/CampaignMechanicsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,34 @@ jest.mock('../components/Campaigns/CampaignHowItWorks', () => {
};
});

jest.mock('../components/ContentfulRichText/ContentfulRichText', () => {
const ReactActual = jest.requireActual('react');
const { View, Text: RNText } = jest.requireActual('react-native');
const isDocumentFn = (value: unknown): boolean =>
value !== null &&
typeof value === 'object' &&
'nodeType' in (value as Record<string, unknown>) &&
(value as Record<string, unknown>).nodeType === 'document' &&
'content' in (value as Record<string, unknown>) &&
Array.isArray((value as Record<string, unknown>).content);
return {
__esModule: true,
isDocument: isDocumentFn,
default: ({
document: doc,
testID,
}: {
document: unknown;
testID?: string;
}) =>
ReactActual.createElement(
View,
{ testID },
ReactActual.createElement(RNText, null, JSON.stringify(doc)),
),
};
});

jest.mock('../hooks/useRewardCampaigns');
const mockUseRewardCampaigns = useRewardCampaigns as jest.MockedFunction<
typeof useRewardCampaigns
Expand Down Expand Up @@ -196,7 +224,21 @@ describe('CampaignMechanicsView', () => {
});

describe('notes section', () => {
it('renders notes section when notes has valid shape', () => {
const richTextNotes = {
nodeType: 'document',
data: {},
content: [
{
nodeType: 'paragraph',
data: {},
content: [
{ nodeType: 'text', value: 'Important notes', marks: [], data: {} },
],
},
],
};

it('renders notes section with ContentfulRichText when notes is present', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [
Expand All @@ -210,39 +252,16 @@ describe('CampaignMechanicsView', () => {
title: 'How it works',
description: 'Earn rewards',
phases: [],
notes: {
title: 'Important notes',
description: 'Please read carefully',
items: [
{ title: 'Note 1', description: 'Detail 1' },
{ title: 'Note 2', description: 'Detail 2' },
],
},
notes: richTextNotes,
},
},
}),
],
});
const { getByTestId, getByText } = render(<CampaignMechanicsView />);
const { getByTestId } = render(<CampaignMechanicsView />);
expect(
getByTestId(CAMPAIGN_MECHANICS_TEST_IDS.NOTES_SECTION),
).toBeDefined();
expect(
getByTestId(CAMPAIGN_MECHANICS_TEST_IDS.NOTES_TITLE),
).toHaveTextContent('Important notes');
expect(
getByTestId(CAMPAIGN_MECHANICS_TEST_IDS.NOTES_DESCRIPTION),
).toHaveTextContent('Please read carefully');
expect(
getByTestId(`${CAMPAIGN_MECHANICS_TEST_IDS.NOTE_ITEM}-0`),
).toBeDefined();
expect(
getByTestId(`${CAMPAIGN_MECHANICS_TEST_IDS.NOTE_ITEM_TITLE}-0`),
).toHaveTextContent('Note 1');
expect(
getByTestId(`${CAMPAIGN_MECHANICS_TEST_IDS.NOTE_ITEM_DESCRIPTION}-0`),
).toHaveTextContent('Detail 1');
expect(getByText('Note 2')).toBeDefined();
});

it('does not render notes section when notes is null', () => {
Expand Down Expand Up @@ -271,7 +290,58 @@ describe('CampaignMechanicsView', () => {
).toBeNull();
});

it('does not render notes section when notes has invalid shape', () => {
it('does not render notes section when howItWorks has no notes field', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [
createTestCampaign({
details: {
image: {
lightModeUrl: 'https://example.com/light.png',
darkModeUrl: 'https://example.com/dark.png',
},
howItWorks: {
title: 'How it works',
description: 'Earn rewards',
phases: [],
},
},
}),
],
});
const { queryByTestId } = render(<CampaignMechanicsView />);
expect(
queryByTestId(CAMPAIGN_MECHANICS_TEST_IDS.NOTES_SECTION),
).toBeNull();
});

it('does not render notes section when notes is a non-document object', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [
createTestCampaign({
details: {
image: {
lightModeUrl: 'https://example.com/light.png',
darkModeUrl: 'https://example.com/dark.png',
},
howItWorks: {
title: 'How it works',
description: 'Earn rewards',
phases: [],
notes: { title: 'Only title' },
},
},
}),
],
});
const { queryByTestId } = render(<CampaignMechanicsView />);
expect(
queryByTestId(CAMPAIGN_MECHANICS_TEST_IDS.NOTES_SECTION),
).toBeNull();
});

it('does not render notes section when notes is a string', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [
Expand All @@ -285,7 +355,7 @@ describe('CampaignMechanicsView', () => {
title: 'How it works',
description: 'Earn rewards',
phases: [],
notes: { title: 'Only title' }, // missing items
notes: 'just a string',
},
},
}),
Expand Down
85 changes: 8 additions & 77 deletions app/components/UI/Rewards/Views/CampaignMechanicsView.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import React, { useMemo } from 'react';
import { ScrollView } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import {
Box,
Text,
TextVariant,
TextColor,
FontWeight,
} from '@metamask/design-system-react-native';
import { Box } from '@metamask/design-system-react-native';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import { SafeAreaView } from 'react-native-safe-area-context';
import HeaderCompactStandard from '../../../../component-library/components-temp/HeaderCompactStandard';
import ErrorBoundary from '../../../Views/ErrorBoundary';
import CampaignHowItWorks from '../components/Campaigns/CampaignHowItWorks';
import ContentfulRichText, {
isDocument,
} from '../components/ContentfulRichText/ContentfulRichText';
import { useRewardCampaigns } from '../hooks/useRewardCampaigns';
import { strings } from '../../../../../locales/i18n';

Expand All @@ -22,41 +19,10 @@ type CampaignMechanicsRouteParams = {
CampaignMechanics: { campaignId: string };
};

interface CampaignNoteItem {
title: string;
description: string;
}

interface CampaignNotes {
title: string;
description: string;
items: CampaignNoteItem[];
}

function parseCampaignNotes(notes: unknown): CampaignNotes | null {
if (
notes !== null &&
typeof notes === 'object' &&
!Array.isArray(notes) &&
'title' in notes &&
'description' in notes &&
'items' in notes &&
Array.isArray((notes as { items: unknown }).items)
) {
return notes as CampaignNotes;
}
return null;
}

export const CAMPAIGN_MECHANICS_TEST_IDS = {
CONTAINER: 'campaign-mechanics-container',
HOW_IT_WORKS_SECTION: 'campaign-mechanics-how-it-works',
NOTES_SECTION: 'campaign-mechanics-notes',
NOTES_TITLE: 'campaign-mechanics-notes-title',
NOTES_DESCRIPTION: 'campaign-mechanics-notes-description',
NOTE_ITEM: 'campaign-mechanics-note-item',
NOTE_ITEM_TITLE: 'campaign-mechanics-note-item-title',
NOTE_ITEM_DESCRIPTION: 'campaign-mechanics-note-item-description',
} as const;

const CampaignMechanicsView: React.FC = () => {
Expand All @@ -73,7 +39,7 @@ const CampaignMechanicsView: React.FC = () => {
);

const howItWorks = campaign?.details?.howItWorks ?? null;
const notes = parseCampaignNotes(howItWorks?.notes);
const notes = howItWorks?.notes ?? null;

return (
<ErrorBoundary navigation={navigation} view="CampaignMechanicsView">
Expand Down Expand Up @@ -101,47 +67,12 @@ const CampaignMechanicsView: React.FC = () => {
</Box>
)}

{notes && (
{isDocument(notes) && (
<Box
twClassName="px-4 py-4 gap-3"
twClassName="px-4 py-4"
testID={CAMPAIGN_MECHANICS_TEST_IDS.NOTES_SECTION}
>
<Text
variant={TextVariant.HeadingMd}
fontWeight={FontWeight.Bold}
testID={CAMPAIGN_MECHANICS_TEST_IDS.NOTES_TITLE}
>
{notes.title}
</Text>
<Text
variant={TextVariant.BodyMd}
color={TextColor.TextAlternative}
testID={CAMPAIGN_MECHANICS_TEST_IDS.NOTES_DESCRIPTION}
>
{notes.description}
</Text>
{notes.items.map((item, index) => (
<Box
key={`note-item-${index}`}
twClassName="gap-1"
testID={`${CAMPAIGN_MECHANICS_TEST_IDS.NOTE_ITEM}-${index}`}
>
<Text
variant={TextVariant.BodyMd}
fontWeight={FontWeight.Bold}
testID={`${CAMPAIGN_MECHANICS_TEST_IDS.NOTE_ITEM_TITLE}-${index}`}
>
{item.title}
</Text>
<Text
variant={TextVariant.BodyMd}
color={TextColor.TextAlternative}
testID={`${CAMPAIGN_MECHANICS_TEST_IDS.NOTE_ITEM_DESCRIPTION}-${index}`}
>
{item.description}
</Text>
</Box>
))}
<ContentfulRichText document={notes} />
Comment thread
cursor[bot] marked this conversation as resolved.
</Box>
)}
</ScrollView>
Expand Down
Loading
Loading