Skip to content

Commit a5b066c

Browse files
committed
Merge branch 'main' into test/temp-nightly
2 parents 4246a4c + ed27aa0 commit a5b066c

149 files changed

Lines changed: 13445 additions & 7325 deletions

File tree

Some content is hidden

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

.agents/skills/component-view-test/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ tests/component-view/
5656
├── mocks.ts ← Engine + native mocks (import this first, always)
5757
├── render.tsx ← renderComponentViewScreen, renderScreenWithRoutes
5858
├── stateFixture.ts ← StateFixtureBuilder (createStateFixture)
59+
├── api-mocking/ ← HTTP API mocks (nock) — extensible, one file per feature
5960
├── presets/ ← initialState<Feature>() builders — one file per feature area
6061
└── renderers/ ← render<Feature>View() functions — one file per feature area
6162
```

.agents/skills/component-view-test/references/navigation-mocking.md

Lines changed: 42 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -134,102 +134,62 @@ Route names live in `app/constants/navigation/Routes.ts`.
134134

135135
## External Service / API Mocking
136136

137-
Some views call external services **directly** (not through Engine controllers) — e.g. a `getTrendingTokens()` function imported from a package, or a `fetch()` call to an external API. These cannot be driven through Redux state overrides.
137+
Some views call **external HTTP APIs** (e.g. `fetch()` to a REST endpoint). Those requests cannot be driven through Redux state. The framework provides an **api-mocking** layer using [nock](https://github.com/nock/nock) so tests intercept HTTP at the network level **without** using `jest.mock` on service modules (which would violate the “only Engine and allowed native mocks” rule).
138138

139-
### Current pattern — jest.mock on the service module
139+
### Preferred pattern — nock (api-mocking folder)
140140

141-
When a view calls an external service function directly, mock the module in a dedicated file under `tests/component-view/mocks/` and expose setup/clear helpers:
141+
All HTTP API mocks for component view tests live under `tests/component-view/api-mocking/`. Each feature has one file (e.g. `trending.ts`) that exports:
142142

143-
```typescript
144-
// tests/component-view/mocks/myFeatureApiMocks.ts
145-
import { getMyFeatureData } from '@metamask/some-package';
146-
147-
export const getMyFeatureDataMock = getMyFeatureData as jest.Mock;
143+
- Mock response data (e.g. `mockTrendingTokensData`)
144+
- A **setup** function (e.g. `setupTrendingApiFetchMock(responseData?, customReply?)`) that uses nock to intercept the endpoint
145+
- A **clear** function (e.g. `clearTrendingApiMocks()`) to call in `afterEach`
148146

149-
export const mockFeatureData = [
150-
{ id: 'item-1', name: 'Token A', price: '100.00', change24h: 5.2 },
151-
{ id: 'item-2', name: 'Token B', price: '200.00', change24h: -1.8 },
152-
];
147+
Shared nock lifecycle helpers (`clearAllNockMocks`, `disableNetConnect`, `teardownNock`) are in `api-mocking/nockHelpers.ts`. To **add a new API mock** for another view, add a file `api-mocking/<feature>.ts` following the pattern in `api-mocking/trending.ts` (mock data, `setupXxxApiMock`, `clearXxxApiMocks` using `nockHelpers`), and call setup/clear in the view test’s `beforeEach`/`afterEach`.
153148

154-
export const setupMyFeatureApiMock = (data = mockFeatureData) => {
155-
getMyFeatureDataMock.mockImplementation(async () => data);
156-
};
157-
158-
export const clearMyFeatureApiMocks = () => {
159-
jest.clearAllMocks();
160-
};
161-
```
162-
163-
In the test file, declare the `jest.mock` at module scope and use `beforeEach`/`afterEach` for lifecycle:
149+
**Example (trending):**
164150

165151
```typescript
166-
// NOTE: antipattern — only Engine and native modules should be mocked in view tests.
167-
// This is a temporary workaround for service functions called directly from components,
168-
// not through Engine. Track removal in the linked issue.
169-
// eslint-disable-next-line no-restricted-syntax
170-
jest.mock('@metamask/some-package', () => {
171-
const actual = jest.requireActual('@metamask/some-package');
172-
return { ...actual, getMyFeatureData: jest.fn().mockResolvedValue([]) };
173-
});
174-
175152
import {
176-
setupMyFeatureApiMock,
177-
clearMyFeatureApiMocks,
178-
mockFeatureData,
179-
getMyFeatureDataMock,
180-
} from '../../../../tests/component-view/mocks/myFeatureApiMocks';
181-
182-
describe('MyFeatureView', () => {
183-
beforeEach(() => {
184-
setupMyFeatureApiMock(mockFeatureData);
185-
});
186-
187-
afterEach(() => {
188-
clearMyFeatureApiMocks();
189-
});
190-
191-
it('shows token list after data loads from the external service', async () => {
192-
const { findByText } = renderMyFeatureWithRoutes();
153+
setupTrendingApiFetchMock,
154+
clearTrendingApiMocks,
155+
mockTrendingTokensData,
156+
mockBnbChainToken,
157+
} from '../../../../tests/component-view/api-mocking/trending';
158+
159+
beforeEach(() => {
160+
setupTrendingApiFetchMock(mockTrendingTokensData);
161+
});
162+
afterEach(() => {
163+
clearTrendingApiMocks();
164+
});
193165

194-
expect(await findByText('Token A')).toBeOnTheScreen();
166+
it('user sees trending tokens section with mocked data', async () => {
167+
const { findByText, queryByTestId } = renderTrendingViewWithRoutes();
168+
await waitFor(async () => {
169+
expect(await findByText('Ethereum')).toBeOnTheScreen();
195170
});
171+
// assert rows with assertTrendingTokenRowsVisibility(...)
172+
});
196173

197-
it('shows only filtered results when a specific param is passed', async () => {
198-
getMyFeatureDataMock.mockImplementation(async (params) => {
199-
if (params?.chainId === 'eip155:56') return [mockBnbData];
200-
return mockFeatureData;
201-
});
202-
203-
const { findByText } = renderMyFeatureWithRoutes();
204-
// ... interact to trigger the filter, then assert
174+
it('displays only BNB tokens when BNB Chain network filter is selected', async () => {
175+
setupTrendingApiFetchMock(mockTrendingTokensData, (uri) => {
176+
const url = new URL(uri, 'https://token.api.cx.metamask.io');
177+
const chainIdsParam = url.searchParams.get('chainIds') ?? '';
178+
const chainIds = chainIdsParam.split(',').map((s) => s.trim());
179+
if (chainIds.length === 1 && chainIds[0] === 'eip155:56') {
180+
return mockBnbChainToken;
181+
}
182+
return mockTrendingTokensData;
205183
});
184+
const { getByTestId, findByText, queryByTestId } =
185+
renderTrendingViewWithRoutes();
186+
// ... navigate to full view, open network filter, select BNB Chain
187+
// assert visible: [BNB], missing: [ETH, BTC, UNI]
206188
});
207189
```
208190

209-
> ⚠️ **This is a known antipattern.** The golden rule is that only Engine and allowed native modules should be mocked in `*.view.test.*` files. Mocking a service module directly bypasses the ESLint guard (note the `eslint-disable` comment). Always link to a tracking issue and plan to migrate to a proper solution.
210-
211-
### Future pattern — Mock Service Worker (MSW)
212-
213-
> 📌 **Placeholder — no example exists yet in this codebase.**
191+
### Fallback — jest.mock on the service module (antipattern)
214192

215-
For views that call HTTP endpoints directly (via `fetch`), the intended approach is [Mock Service Worker (msw)](https://mswjs.io/), which intercepts requests at the network level without needing `jest.mock`. This keeps tests closer to real behavior and avoids the module-mock antipattern.
193+
When a view calls an external **function** (not `fetch`) from a package and that function cannot be replaced by nock (e.g. no HTTP), you may mock the module in a file under `tests/component-view/mocks/` and use setup/clear helpers. This requires an `eslint-disable` and is a **known antipattern**; prefer moving the integration to an HTTP API and using api-mocking, or drive data through Engine/Redux when possible.
216194

217-
When the first MSW-based view test is written, document the setup here:
218-
219-
```typescript
220-
// TODO: Add MSW setup example once the first test using it is merged.
221-
// Expected shape:
222-
//
223-
// import { setupServer } from 'msw/node';
224-
// import { http, HttpResponse } from 'msw';
225-
//
226-
// const server = setupServer(
227-
// http.get('https://api.example.com/tokens', () =>
228-
// HttpResponse.json(mockTokensData),
229-
// ),
230-
// );
231-
//
232-
// beforeAll(() => server.listen());
233-
// afterEach(() => server.resetHandlers());
234-
// afterAll(() => server.close());
235-
```
195+
> ⚠️ Only Engine and allowed native modules should be mocked in `*.view.test.*` files. Mocking a service module directly bypasses the ESLint guard. Always link to a tracking issue and plan to migrate to nock (api-mocking) or Engine/Redux.

.agents/skills/component-view-test/references/reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ yarn eslint <path/to/test.tsx>
203203
| Engine + native mocks | `tests/component-view/mocks.ts` |
204204
| render, renderScreenWithRoutes | `tests/component-view/render.tsx` |
205205
| StateFixtureBuilder | `tests/component-view/stateFixture.ts` |
206+
| HTTP API mocks (nock) | `tests/component-view/api-mocking/` (per-feature) |
206207
| Feature renderers (per view) | `tests/component-view/renderers/` (e.g. bridge, wallet) |
207208
| Feature presets (per view) | `tests/component-view/presets/` (e.g. bridge, wallet) |
208209
| DeepPartial type | `app/util/test/renderWithProvider` |

.agents/skills/component-view-test/references/writing-tests.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Before writing any test, read:
1212
- Any existing `*.view.test.tsx` for the same component
1313
- The relevant preset(s) in `tests/component-view/presets/`
1414
- The relevant renderer(s) in `tests/component-view/renderers/`
15+
- If the view calls an external HTTP API: `tests/component-view/api-mocking/` and any existing `api-mocking/<feature>.ts` for that API (see navigation-mocking.md, External Service / API Mocking)
1516

1617
---
1718

.github/workflows/build.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ on:
1313
required: false
1414
type: boolean
1515
default: false
16+
ref:
17+
description: 'Git ref to checkout when skip_version_bump is true. Defaults to the triggering event ref.'
18+
required: false
19+
type: string
20+
default: ''
1621
workflow_dispatch:
1722
inputs:
1823
build_name:
@@ -99,6 +104,7 @@ jobs:
99104
setup-dependencies:
100105
name: Setup Dependencies (${{ matrix.platform }})
101106
needs: [prepare]
107+
if: ${{ always() && !failure() && !cancelled() }}
102108
strategy:
103109
matrix:
104110
platform: ${{ inputs.platform == 'both' && fromJSON('["android", "ios"]') || fromJSON(format('["{0}"]', inputs.platform)) }}
@@ -140,7 +146,13 @@ jobs:
140146
submodules: recursive
141147

142148
- uses: actions/checkout@v4
143-
if: ${{ inputs.skip_version_bump }}
149+
if: ${{ inputs.skip_version_bump && inputs.ref != '' }}
150+
with:
151+
ref: ${{ inputs.ref }}
152+
submodules: recursive
153+
154+
- uses: actions/checkout@v4
155+
if: ${{ inputs.skip_version_bump && inputs.ref == '' }}
144156
with:
145157
submodules: recursive
146158

.github/workflows/nightly-build.yml

Lines changed: 139 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,170 @@ name: Nightly Build
22

33
# Triggered by every push to chore/temp-nightly (which nightly-temp-branch-sync.yml
44
# force-pushes daily at 4 AM UTC to match main).
5+
# Temporarily now this is pointing to test/temp-nightly instead of chore/temp-nightly.
56
#
6-
# [skip ci] commits (e.g. version bumps pushed by Bitrise's bump_version_code job via
7-
# update-latest-build-version.yml) are automatically skipped by GitHub Actions, so
8-
# this workflow will NOT double-trigger on those commits.
9-
#
10-
# skip_version_bump=true is passed to build.yml because Bitrise already owns the
11-
# version bump for chore/temp-nightly during the parallel transition period.
12-
# When Bitrise is deprecated, remove skip_version_bump: true and the version bump
13-
# will be handled by build.yml as normal.
7+
# [skip ci] commits (e.g. version bumps pushed via update-latest-build-version.yml)
8+
# are automatically skipped by GitHub Actions, so this workflow will NOT
9+
# double-trigger on those commits.
1410

1511
on:
1612
push:
1713
branches:
18-
- chore/temp-nightly
14+
- test/temp-nightly
1915
workflow_dispatch:
2016

17+
# contents: write required by build.yml update-build-version job (version bump commit push)
2118
permissions:
22-
contents: read
19+
contents: write
2320
id-token: write
2421

2522
jobs:
23+
bump-version:
24+
name: Bump build version
25+
uses: ./.github/workflows/update-latest-build-version.yml
26+
permissions:
27+
contents: write
28+
id-token: write
29+
with:
30+
base-branch: ${{ github.ref_name }}
31+
secrets: inherit
32+
2633
build-exp:
2734
name: Nightly exp build (main-exp)
35+
needs: [bump-version]
2836
uses: ./.github/workflows/build.yml
2937
with:
3038
build_name: main-exp
3139
platform: both
3240
skip_version_bump: true
41+
ref: ${{ needs.bump-version.outputs.commit-hash }}
3342
secrets: inherit
3443

3544
build-rc:
3645
name: Nightly RC build (main-rc)
46+
needs: [bump-version]
3747
uses: ./.github/workflows/build.yml
3848
with:
3949
build_name: main-rc
4050
platform: both
4151
skip_version_bump: true
52+
ref: ${{ needs.bump-version.outputs.commit-hash }}
4253
secrets: inherit
54+
55+
upload-exp-testflight:
56+
name: Upload exp to TestFlight
57+
needs: [build-exp]
58+
runs-on: ghcr.io/cirruslabs/macos-runner:sequoia-xl
59+
environment: apple
60+
steps:
61+
- name: Checkout repository
62+
uses: actions/checkout@v4
63+
with:
64+
fetch-depth: 0
65+
66+
- name: Setup Ruby (iOS)
67+
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb #v1
68+
with:
69+
ruby-version: '3.2.9'
70+
working-directory: ios
71+
bundler-cache: true
72+
73+
- name: Download iOS build artifact
74+
uses: actions/download-artifact@v4
75+
with:
76+
name: ios-main-exp
77+
78+
- name: Find IPA path
79+
id: ipa
80+
run: |
81+
IPA=$(find . -name '*.ipa' -type f | head -1)
82+
if [ -z "$IPA" ]; then
83+
echo "::error::No .ipa file found in artifact"
84+
exit 1
85+
fi
86+
case "$IPA" in /*) ABS="$IPA" ;; *) ABS="$PWD/$IPA" ;; esac
87+
echo "path=$ABS" >> "$GITHUB_OUTPUT"
88+
89+
- name: Setup App Store Connect API Key
90+
run: |
91+
bash scripts/setup-app-store-connect-api-key.sh \
92+
"$APP_STORE_CONNECT_API_KEY_ISSUER_ID" \
93+
"$APP_STORE_CONNECT_API_KEY_KEY_ID" \
94+
"$APP_STORE_CONNECT_API_KEY_KEY_CONTENT"
95+
env:
96+
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
97+
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
98+
APP_STORE_CONNECT_API_KEY_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_CONTENT }}
99+
100+
- name: Upload to TestFlight
101+
run: |
102+
bash scripts/upload-to-testflight.sh \
103+
"github_actions_main-exp" \
104+
"${{ github.ref_name }}" \
105+
"${{ steps.ipa.outputs.path }}" \
106+
"MetaMask BETA & Release Candidates"
107+
108+
- name: Cleanup API Key
109+
if: always()
110+
run: |
111+
rm -f ios/AuthKey.p8
112+
echo "🧹 Cleaned up API key file"
113+
114+
upload-rc-testflight:
115+
name: Upload RC to TestFlight
116+
needs: [build-rc]
117+
runs-on: ghcr.io/cirruslabs/macos-runner:sequoia-xl
118+
environment: apple
119+
steps:
120+
- name: Checkout repository
121+
uses: actions/checkout@v4
122+
with:
123+
fetch-depth: 0
124+
125+
- name: Setup Ruby (iOS)
126+
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb #v1
127+
with:
128+
ruby-version: '3.2.9'
129+
working-directory: ios
130+
bundler-cache: true
131+
132+
- name: Download iOS build artifact
133+
uses: actions/download-artifact@v4
134+
with:
135+
name: ios-main-rc
136+
137+
- name: Find IPA path
138+
id: ipa
139+
run: |
140+
IPA=$(find . -name '*.ipa' -type f | head -1)
141+
if [ -z "$IPA" ]; then
142+
echo "::error::No .ipa file found in artifact"
143+
exit 1
144+
fi
145+
case "$IPA" in /*) ABS="$IPA" ;; *) ABS="$PWD/$IPA" ;; esac
146+
echo "path=$ABS" >> "$GITHUB_OUTPUT"
147+
148+
- name: Setup App Store Connect API Key
149+
run: |
150+
bash scripts/setup-app-store-connect-api-key.sh \
151+
"$APP_STORE_CONNECT_API_KEY_ISSUER_ID" \
152+
"$APP_STORE_CONNECT_API_KEY_KEY_ID" \
153+
"$APP_STORE_CONNECT_API_KEY_KEY_CONTENT"
154+
env:
155+
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
156+
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
157+
APP_STORE_CONNECT_API_KEY_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_CONTENT }}
158+
159+
- name: Upload to TestFlight
160+
run: |
161+
bash scripts/upload-to-testflight.sh \
162+
"github_actions_main-rc" \
163+
"${{ github.ref_name }}" \
164+
"${{ steps.ipa.outputs.path }}" \
165+
"MetaMask BETA & Release Candidates"
166+
167+
- name: Cleanup API Key
168+
if: always()
169+
run: |
170+
rm -f ios/AuthKey.p8
171+
echo "🧹 Cleaned up API key file"

.github/workflows/push-eas-update.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,9 @@ jobs:
277277
EXPO_PROJECT_ID: ${{ secrets.EXPO_PROJECT_ID }}
278278
EXPO_CHANNEL: ${{ vars.EXPO_CHANNEL }}
279279
GIT_BRANCH: ${{ github.ref_name }}
280-
RAMP_INTERNAL_BUILD: 'false'
280+
RAMP_DEV_BUILD: ${{ secrets.RAMP_DEV_BUILD || 'false' }}
281+
RAMP_INTERNAL_BUILD: ${{ secrets.RAMP_INTERNAL_BUILD || 'false' }}
282+
RAMPS_ENVIRONMENT: ${{ secrets.RAMPS_ENVIRONMENT || 'production' }}
281283
MM_MUSD_CONVERSION_FLOW_ENABLED: 'false'
282284
MM_NETWORK_UI_REDESIGN_ENABLED: 'false'
283285
MM_NOTIFICATIONS_UI_ENABLED: 'true'

0 commit comments

Comments
 (0)