Skip to content

Commit e72f6bb

Browse files
authored
Merge branch 'rn-upgrade/0.81.5-no-unit-tests' into chore/drop-snaps-xmlserializer-patch
2 parents 1403e08 + 61b253a commit e72f6bb

47 files changed

Lines changed: 1168 additions & 241 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/smart-e2e-selection/action.yml

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ inputs:
2727
required: false
2828
default: 'false'
2929
base-ref:
30-
description: 'PR base branch ref passed to the AI analysis script for diff comparison'
30+
description: 'PR base branch ref passed to the AI analysis script for diff comparison. When release/*, AI selection is skipped and the full E2E suite is selected.'
3131
required: false
3232
default: ''
3333
outputs:
@@ -57,21 +57,32 @@ runs:
5757
echo "⏭️ SKIP=true due to 'skip-smart-e2e-selection' label on PR"
5858
fi
5959
60+
- name: Check release target branch (full E2E, no AI selection)
61+
id: check-release-target
62+
shell: bash
63+
run: |
64+
echo "SKIP=false" >> "$GITHUB_OUTPUT"
65+
BASE='${{ inputs.base-ref }}'
66+
if [[ -n "$BASE" && "$BASE" == release/* ]]; then
67+
echo "SKIP=true" >> "$GITHUB_OUTPUT"
68+
echo "⏭️ Base branch is release/* — skipping AI E2E selection; full E2E suite will run"
69+
fi
70+
6071
- name: Checkout for PR analysis
61-
if: steps.check-skip-label.outputs.SKIP != 'true'
72+
if: steps.check-skip-label.outputs.SKIP != 'true' && steps.check-release-target.outputs.SKIP != 'true'
6273
uses: actions/checkout@v6
6374
with:
6475
fetch-depth: 1 # Shallow clone for speed; unshallowed below for diff comparison
6576

6677
- name: Disable sparse checkout and restore all files
67-
if: steps.check-skip-label.outputs.SKIP != 'true'
78+
if: steps.check-skip-label.outputs.SKIP != 'true' && steps.check-release-target.outputs.SKIP != 'true'
6879
shell: bash
6980
run: |
7081
git sparse-checkout disable
7182
git checkout HEAD -- .
7283
7384
- name: Fetch base branch for comparison
74-
if: steps.check-skip-label.outputs.SKIP != 'true'
85+
if: steps.check-skip-label.outputs.SKIP != 'true' && steps.check-release-target.outputs.SKIP != 'true'
7586
shell: bash
7687
run: |
7788
# Unshallow the repository first (if it's shallow)
@@ -80,13 +91,13 @@ runs:
8091
git fetch origin "${{ inputs.base-ref || 'main' }}" 2>/dev/null || true
8192
8293
- name: Setup Node.js
83-
if: steps.check-skip-label.outputs.SKIP != 'true'
94+
if: steps.check-skip-label.outputs.SKIP != 'true' && steps.check-release-target.outputs.SKIP != 'true'
8495
uses: actions/setup-node@v6
8596
with:
8697
node-version-file: '.nvmrc'
8798

8899
- name: Install minimal dependencies for AI analysis
89-
if: steps.check-skip-label.outputs.SKIP != 'true'
100+
if: steps.check-skip-label.outputs.SKIP != 'true' && steps.check-release-target.outputs.SKIP != 'true'
90101
shell: bash
91102
run: |
92103
echo "📦 Installing only required packages for AI analysis..."
@@ -98,7 +109,7 @@ runs:
98109
echo "✅ AI analysis dependencies installed in /tmp/ai-deps"
99110
100111
- name: Copy AI dependencies to workspace
101-
if: steps.check-skip-label.outputs.SKIP != 'true'
112+
if: steps.check-skip-label.outputs.SKIP != 'true' && steps.check-release-target.outputs.SKIP != 'true'
102113
shell: bash
103114
run: |
104115
echo "📋 Copying AI dependencies to workspace..."
@@ -130,6 +141,11 @@ runs:
130141
echo "SKIPPED=true" >> "$GITHUB_OUTPUT"
131142
echo "SKIP_REASON=skip-smart-e2e-selection label found" >> "$GITHUB_OUTPUT"
132143
echo "ai_confidence=100" >> "$GITHUB_OUTPUT"
144+
elif [[ "${{ steps.check-release-target.outputs.SKIP }}" == "true" ]]; then
145+
echo "⏭️ Skipping AI analysis - PR targets a release branch (release/*)"
146+
echo "SKIPPED=true" >> "$GITHUB_OUTPUT"
147+
echo "SKIP_REASON=PR targets a release branch (release/*)" >> "$GITHUB_OUTPUT"
148+
echo "ai_confidence=100" >> "$GITHUB_OUTPUT"
133149
else
134150
echo "✅ Running AI analysis for PR #$PR_NUMBER"
135151
# The script will generate the GH output variables - don't fail if script errors
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React from 'react';
2+
import { render, fireEvent, waitFor, act } from '@testing-library/react-native';
3+
import { useSelector } from 'react-redux';
4+
import CampaignReminder from './CampaignReminder';
5+
import { reminderStorageKeyForComposite } from '../../hooks/useCampaignReminderSubscriptions';
6+
import {
7+
type CampaignDto,
8+
CampaignType,
9+
} from '../../../../../core/Engine/controllers/rewards-controller/types';
10+
import { MetaMetricsEvents } from '../../../../../core/Analytics';
11+
import { selectRewardsSubscriptionId } from '../../../../../selectors/rewards';
12+
13+
const mockTrackEvent = jest.fn();
14+
const mockCreateEventBuilder = jest.fn();
15+
const mockShowToast = jest.fn();
16+
17+
const TEST_REWARDS_SUBSCRIPTION_ID = 'test-rewards-sub-id';
18+
19+
const mockGetItemSync = jest.fn((_key: string): string | null => null);
20+
const mockSetItem = jest.fn(
21+
(_key: string, _value: string): Promise<void> => Promise.resolve(),
22+
);
23+
24+
jest.mock('react-redux', () => ({
25+
...jest.requireActual('react-redux'),
26+
useSelector: jest.fn(),
27+
}));
28+
29+
const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
30+
31+
jest.mock('../../../../../store/storage-wrapper', () => ({
32+
__esModule: true,
33+
default: {
34+
getItemSync: (key: string) => mockGetItemSync(key),
35+
setItem: (key: string, value: string) => mockSetItem(key, value),
36+
},
37+
}));
38+
39+
jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({
40+
useAnalytics: jest.fn(() => ({
41+
trackEvent: mockTrackEvent,
42+
createEventBuilder: mockCreateEventBuilder,
43+
})),
44+
}));
45+
46+
jest.mock('../../hooks/useRewardsToast', () => ({
47+
__esModule: true,
48+
default: jest.fn(() => ({
49+
showToast: mockShowToast,
50+
RewardsToastOptions: {
51+
success: jest.fn((title: string, subtitle?: string) => ({
52+
variant: 'success',
53+
title,
54+
subtitle,
55+
})),
56+
error: jest.fn((title: string, subtitle?: string) => ({
57+
variant: 'error',
58+
title,
59+
subtitle,
60+
})),
61+
entriesClosed: jest.fn(),
62+
},
63+
})),
64+
}));
65+
66+
jest.mock('../../../../../images/rewards/notification.svg', () => {
67+
const ReactActual = jest.requireActual('react');
68+
const { View } = jest.requireActual('react-native');
69+
return {
70+
__esModule: true,
71+
default: () =>
72+
ReactActual.createElement(View, { testID: 'campaign-reminder-svg' }),
73+
};
74+
});
75+
76+
jest.mock('@metamask/design-system-react-native', () => {
77+
const actual = jest.requireActual('@metamask/design-system-react-native');
78+
return { ...actual };
79+
});
80+
81+
jest.mock('@metamask/design-system-twrnc-preset', () => ({
82+
useTailwind: () => ({ style: (...args: unknown[]) => args }),
83+
}));
84+
85+
jest.mock('../../../../../../locales/i18n', () => ({
86+
strings: (key: string) => {
87+
const translations: Record<string, string> = {
88+
'rewards.campaign.up_next': 'Up next',
89+
'rewards.campaign.notify_me': 'Notify me',
90+
'rewards.campaign.remind_me_success_toast': 'We will notify you.',
91+
'rewards.campaign.remind_me_save_error': 'Save failed.',
92+
};
93+
return translations[key] || key;
94+
},
95+
}));
96+
97+
const createTestCampaign = (overrides = {}): CampaignDto => ({
98+
id: 'campaign-reminder-1',
99+
type: CampaignType.ONDO_HOLDING,
100+
name: 'Preview Campaign',
101+
startDate: '2028-01-01T00:00:00.000Z',
102+
endDate: '2028-12-31T23:59:59.999Z',
103+
termsAndConditions: null,
104+
excludedRegions: [],
105+
details: null,
106+
featured: true,
107+
showUpcomingDate: false,
108+
...overrides,
109+
});
110+
111+
describe('CampaignReminder', () => {
112+
beforeEach(() => {
113+
jest.clearAllMocks();
114+
mockGetItemSync.mockReturnValue(null);
115+
mockSetItem.mockResolvedValue(undefined);
116+
mockUseSelector.mockImplementation((selector) => {
117+
if (selector === selectRewardsSubscriptionId) {
118+
return TEST_REWARDS_SUBSCRIPTION_ID;
119+
}
120+
return undefined;
121+
});
122+
mockCreateEventBuilder.mockImplementation(() => {
123+
const builder = {
124+
addProperties: jest.fn(),
125+
build: jest.fn(() => ({})),
126+
};
127+
(builder.addProperties as jest.Mock).mockReturnValue(builder);
128+
return builder;
129+
});
130+
});
131+
132+
it('renders up next label and campaign name', async () => {
133+
const campaign = createTestCampaign({ name: 'My Upcoming Campaign' });
134+
const { getByText, getByTestId } = render(
135+
<CampaignReminder campaign={campaign} />,
136+
);
137+
138+
await waitFor(() => {
139+
expect(
140+
getByTestId('campaign-reminder-notify-campaign-reminder-1'),
141+
).toBeOnTheScreen();
142+
});
143+
expect(getByText('Up next')).toBeOnTheScreen();
144+
expect(getByText('My Upcoming Campaign')).toBeOnTheScreen();
145+
expect(getByText('Notify me')).toBeOnTheScreen();
146+
});
147+
148+
it('tracks reminder subscribed when Notify me is pressed', async () => {
149+
const campaign = createTestCampaign({ id: 'cr-analytics' });
150+
const { getByTestId } = render(<CampaignReminder campaign={campaign} />);
151+
152+
await waitFor(() => {
153+
expect(
154+
getByTestId('campaign-reminder-notify-cr-analytics'),
155+
).toBeOnTheScreen();
156+
});
157+
158+
await act(async () => {
159+
fireEvent.press(getByTestId('campaign-reminder-notify-cr-analytics'));
160+
});
161+
162+
expect(mockSetItem).toHaveBeenCalledWith(
163+
reminderStorageKeyForComposite(
164+
`${TEST_REWARDS_SUBSCRIPTION_ID}:cr-analytics`,
165+
),
166+
'1',
167+
);
168+
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
169+
MetaMetricsEvents.REWARDS_CAMPAIGN_REMINDER_SUBSCRIBED,
170+
);
171+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
172+
expect(mockShowToast).toHaveBeenCalledTimes(1);
173+
});
174+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React from 'react';
2+
import { Pressable } from 'react-native';
3+
import {
4+
Box,
5+
BoxAlignItems,
6+
BoxFlexDirection,
7+
BoxJustifyContent,
8+
Text,
9+
TextColor,
10+
TextVariant,
11+
FontWeight,
12+
} from '@metamask/design-system-react-native';
13+
import { useTailwind } from '@metamask/design-system-twrnc-preset';
14+
import { useTheme } from '../../../../../util/theme';
15+
import NotificationIcon from '../../../../../images/rewards/notification.svg';
16+
import type { CampaignDto } from '../../../../../core/Engine/controllers/rewards-controller/types';
17+
import { strings } from '../../../../../../locales/i18n';
18+
import { isCampaignTypeSupported } from './CampaignTile.utils';
19+
import { useCampaignReminderActions } from '../../hooks/useCampaignReminderActions';
20+
21+
interface CampaignReminderProps {
22+
campaign: CampaignDto;
23+
}
24+
25+
/**
26+
* Compact preview row for an upcoming featured campaign: label, name, and
27+
* the same reminder flow as {@link CampaignTile}.
28+
*/
29+
const CampaignReminder: React.FC<CampaignReminderProps> = ({ campaign }) => {
30+
const tw = useTailwind();
31+
const { colors } = useTheme();
32+
const reminderEnabled = isCampaignTypeSupported(campaign.type);
33+
const { showRemindMeCta, handleRemindMePress } = useCampaignReminderActions(
34+
campaign,
35+
reminderEnabled,
36+
);
37+
38+
return (
39+
<Box
40+
flexDirection={BoxFlexDirection.Row}
41+
alignItems={BoxAlignItems.Center}
42+
justifyContent={BoxJustifyContent.Between}
43+
twClassName="rounded-xl bg-muted p-4 gap-3"
44+
testID={`campaign-reminder-${campaign.id}`}
45+
>
46+
<Box
47+
flexDirection={BoxFlexDirection.Column}
48+
twClassName="min-w-0 flex-1 shrink gap-0.5"
49+
>
50+
<Text
51+
variant={TextVariant.BodySm}
52+
color={TextColor.TextAlternative}
53+
fontWeight={FontWeight.Medium}
54+
>
55+
{strings('rewards.campaign.up_next')}
56+
</Text>
57+
<Text
58+
variant={TextVariant.HeadingSm}
59+
color={TextColor.TextDefault}
60+
fontWeight={FontWeight.Medium}
61+
numberOfLines={2}
62+
>
63+
{campaign.name}
64+
</Text>
65+
</Box>
66+
{showRemindMeCta && (
67+
<Pressable
68+
onPress={() => {
69+
handleRemindMePress().catch(() => undefined);
70+
}}
71+
testID={`campaign-reminder-notify-${campaign.id}`}
72+
accessibilityRole="button"
73+
accessibilityLabel={strings('rewards.campaign.notify_me')}
74+
style={({ pressed }) =>
75+
tw.style(
76+
'flex-row items-center gap-1.5 rounded-lg px-4 py-3 bg-background-muted',
77+
pressed && 'opacity-70',
78+
)
79+
}
80+
>
81+
<NotificationIcon
82+
name="notification"
83+
width={20}
84+
height={20}
85+
color={colors.icon.default}
86+
/>
87+
<Text
88+
variant={TextVariant.BodySm}
89+
color={TextColor.TextDefault}
90+
fontWeight={FontWeight.Medium}
91+
>
92+
{strings('rewards.campaign.notify_me')}
93+
</Text>
94+
</Pressable>
95+
)}
96+
</Box>
97+
);
98+
};
99+
100+
export default CampaignReminder;

0 commit comments

Comments
 (0)