Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
66 changes: 38 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,26 @@ jest.mock('../components/Campaigns/CampaignHowItWorks', () => {
};
});

jest.mock('../components/ContentfulRichText/ContentfulRichText', () => {
const ReactActual = jest.requireActual('react');
const { View, Text: RNText } = jest.requireActual('react-native');
return {
__esModule: true,
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 +216,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 +244,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 +282,7 @@ 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: [
Expand All @@ -285,7 +296,6 @@ describe('CampaignMechanicsView', () => {
title: 'How it works',
description: 'Earn rewards',
phases: [],
notes: { title: 'Only title' }, // missing items
},
},
}),
Expand Down
81 changes: 5 additions & 76 deletions app/components/UI/Rewards/Views/CampaignMechanicsView.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
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 from '../components/ContentfulRichText/ContentfulRichText';
import { useRewardCampaigns } from '../hooks/useRewardCampaigns';
import { strings } from '../../../../../locales/i18n';

Expand All @@ -22,41 +17,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 +37,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 @@ -103,45 +67,10 @@ const CampaignMechanicsView: React.FC = () => {

{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
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { Linking } from 'react-native';
import CampaignOptInSheet from './CampaignOptInSheet';
import {
type CampaignDto,
CampaignType,
} from '../../../../../core/Engine/controllers/rewards-controller/types';
import { useOptInToCampaign } from '../../hooks/useOptInToCampaign';
import Routes from '../../../../../constants/navigation/Routes';

const mockNavigate = jest.fn();
jest.mock('@react-navigation/native', () => ({
useNavigation: () => ({ navigate: mockNavigate }),
}));

jest.mock('@metamask/design-system-react-native', () => {
const actual = jest.requireActual('@metamask/design-system-react-native');
Expand Down Expand Up @@ -63,6 +68,26 @@
};
});

jest.mock('../ContentfulRichText/ContentfulRichText', () => {
const ReactActual = jest.requireActual('react');
const { View, Text: RNText } = jest.requireActual('react-native');
return {
__esModule: true,
default: ({
document: doc,
testID,
}: {
document: unknown;
testID?: string;
}) =>
ReactActual.createElement(
View,
{ testID },
ReactActual.createElement(RNText, null, JSON.stringify(doc)),
),
};
});

jest.mock('../Onboarding/constants', () => ({
REWARDS_ONBOARD_TERMS_URL: 'https://go.metamask.io/rewards-terms',
}));
Expand Down Expand Up @@ -103,7 +128,6 @@
describe('CampaignOptInSheet', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(Linking, 'openURL').mockResolvedValue(undefined);
mockUseOptInToCampaign.mockReturnValue({
optInToCampaign: mockOptInToCampaign,
isOptingIn: false,
Expand Down Expand Up @@ -137,14 +161,17 @@
);
});

it('opens the terms URL when terms link is pressed', () => {
it('opens the terms URL in in-app browser when terms link is pressed', () => {
const { getByTestId } = render(
<CampaignOptInSheet campaign={createTestCampaign()} />,
);
fireEvent.press(getByTestId('campaign-opt-in-sheet-terms-link'));
expect(Linking.openURL).toHaveBeenCalledWith(
'https://go.metamask.io/rewards-terms',
);
expect(mockNavigate).toHaveBeenCalledWith(Routes.BROWSER.HOME, {
screen: Routes.BROWSER.VIEW,
params: expect.objectContaining({
newTabUrl: 'https://go.metamask.io/rewards-terms',
}),
});
});

it('renders the CTA button', () => {
Expand Down Expand Up @@ -234,4 +261,59 @@
// Button still renders while loading
expect(getByTestId('campaign-opt-in-cta')).toBeDefined();
});

describe('termsAndConditions rich text', () => {
const richTextDoc = {
nodeType: 'document',
data: {},
content: [
{
nodeType: 'paragraph',
data: {},
content: [
{
nodeType: 'text',
value: 'By joining you agree to the ',
marks: [],
data: {},
},
{
nodeType: 'hyperlink',
data: { uri: 'https://example.com/terms' },
content: [
{ nodeType: 'text', value: 'Terms', marks: [], data: {} },
],
},
],
},
],
};

it('renders ContentfulRichText when termsAndConditions is present', () => {
const { getByTestId } = render(
<CampaignOptInSheet
campaign={createTestCampaign({ termsAndConditions: richTextDoc })}

Check failure on line 295 in app/components/UI/Rewards/components/Campaigns/CampaignOptInSheet.test.tsx

View workflow job for this annotation

GitHub Actions / scripts (lint:tsc)

Type '{ nodeType: string; data: {}; content: { nodeType: string; data: {}; content: ({ nodeType: string; value: string; marks: never[]; data: { uri?: undefined; }; content?: undefined; } | { nodeType: string; data: { ...; }; content: { ...; }[]; value?: undefined; marks?: undefined; })[]; }[]; }' is not assignable to type 'Json | undefined'.
/>,
);
expect(getByTestId('campaign-opt-in-sheet-description')).toBeDefined();
Comment thread
cursor[bot] marked this conversation as resolved.
});

it('does not render the static terms link when termsAndConditions is present', () => {
const { queryByTestId } = render(
<CampaignOptInSheet
campaign={createTestCampaign({ termsAndConditions: richTextDoc })}

Check failure on line 304 in app/components/UI/Rewards/components/Campaigns/CampaignOptInSheet.test.tsx

View workflow job for this annotation

GitHub Actions / scripts (lint:tsc)

Type '{ nodeType: string; data: {}; content: { nodeType: string; data: {}; content: ({ nodeType: string; value: string; marks: never[]; data: { uri?: undefined; }; content?: undefined; } | { nodeType: string; data: { ...; }; content: { ...; }[]; value?: undefined; marks?: undefined; })[]; }[]; }' is not assignable to type 'Json | undefined'.
/>,
);
expect(queryByTestId('campaign-opt-in-sheet-terms-link')).toBeNull();
});

it('renders the static fallback when termsAndConditions is null', () => {
const { getByTestId } = render(
<CampaignOptInSheet
campaign={createTestCampaign({ termsAndConditions: null })}
/>,
);
expect(getByTestId('campaign-opt-in-sheet-terms-link')).toBeDefined();
});
});
});
Loading
Loading