Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6fe5eee
refactor: remove horizontal and vertical fades from homepage (#27203)
wachunei Mar 10, 2026
348c47e
fix(homepage): revert perps position row to PerpsCard component (#27206)
wachunei Mar 10, 2026
8ecb0f9
feat(perps): add MM Pay token metrics and cancel trade event tracking…
michalconsensys Mar 10, 2026
aae232a
feat(perps): ensure PERPS_SCREEN_VIEWED source reflects screen entry …
michalconsensys Mar 10, 2026
8c0048a
feat: move price impact threshold to LD flags (#27196)
GeorgeGkas Mar 10, 2026
f2e0a74
fix: hide account balances in account list when privacy mode is enabl…
vinnyhoward Mar 10, 2026
ce3ef6d
fix: redirect deeplink (#26879)
ieow Mar 10, 2026
50023ec
feat(perps): add Sentry tracing for order by payment token and deposi…
michalconsensys Mar 10, 2026
67f9423
fix(perps): use safe provider access to prevent CLIENT_NOT_INITIALIZE…
abretonc7s Mar 10, 2026
e6834dd
fix(perps): reduce Sentry noise from handled perpDexs() transient fai…
abretonc7s Mar 10, 2026
9be521c
test: add back Trending CV test (#27129)
Prithpal-Sooriya Mar 10, 2026
b528bec
fix(perps): ensure Arbitrum exists before deposit when Long/Short fro…
aganglada Mar 10, 2026
cb5b580
chore: Refine section styling across Perps, Explore and Predictions (…
amandaye0h Mar 10, 2026
8155502
feat: Add fiat payment method highlighted actions for PayWithModal (#…
OGPoyraz Mar 10, 2026
ecc2860
docs(skill): add component-view-test Claude Code skill (#27013)
racitores Mar 10, 2026
ed9e0fd
feat(card): add BaanxProvider and BaanxService (#27160)
Brunonascdev Mar 10, 2026
0396fe2
refactor: simplify TransactionDetailsSummary into modular components …
matthewwalsh0 Mar 10, 2026
e1d9a57
refactor(analytics): PR A1 move SegmentPersistor, PrivacyPlugin, cons…
NicolasMassart Mar 10, 2026
61fae26
fix(homepage): prediction carousel no longer swipes card by card on (…
vinnyhoward Mar 10, 2026
5f851ad
fix: price impact modal content (#27256)
GeorgeGkas Mar 10, 2026
ae7313e
fix: render quote rate info modal (#27249)
GeorgeGkas Mar 10, 2026
0bbf6f0
test: file manualbackupstep3 add comprehensive test and remove dead c…
tylerc-consensys Mar 10, 2026
7ec754a
fix: pin seed phrase font size to prevent shrinking on large font dev…
tylerc-consensys Mar 10, 2026
b01640b
test: add feature flag registry (#26913)
pnarayanaswamy Mar 10, 2026
33262f4
chore: remove stx status page triggers from stx publish hook (#27217)
infiniteflower Mar 10, 2026
a559161
feat(tron): display TRX ready for withdrawal (#27075)
ulissesferreira Mar 10, 2026
289f897
chore: bump assets controllers to v100.2.0 (#27228)
salimtb Mar 10, 2026
dc62a84
feat: skeleton migration (swaps scope) (#27204)
kirillzyusko Mar 10, 2026
2a5aeb3
feat: migrate Label (card scope) (#27255)
kirillzyusko Mar 10, 2026
cb287f7
fix: end trace when asset is not available (#27131)
joaosantos15 Mar 10, 2026
5dfa0b8
fix: surface api errors and improve resend UX on OTP screen (#26727)
georgeweiler Mar 10, 2026
d8a188b
refactor: updated AI icon to the filled version (#27273)
brianacnguyen Mar 10, 2026
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
135 changes: 135 additions & 0 deletions .agents/skills/component-view-test/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
name: component-view-test
description:
Write, fix, and update component view tests (*.view.test.tsx) for MetaMask
Mobile using the tests/component-view/ framework. Use when creating a new view test
file, fixing a failing view test, updating tests after a component change, or creating
a new renderer or preset for a view.
---

# Component View Test Agent

**Goal**: Create, update, and fix component view tests (`*.view.test.tsx`) in the MetaMask Mobile codebase using the `tests/component-view/` framework.

Use this skill whenever you need to:

- Write a new component view test file
- Update tests after a component or preset has changed
- Diagnose and fix a failing component view test

Your job is to figure out whether the user needs to **write a new test**, **fix a failing test**, or **update tests after a component/preset change**, then follow the corresponding path and open the relevant reference when that path indicates.

**Decision tree — which reference to use:**

```
Task → What do you need?
├─ Write new test or update after change
│ → Read component + existing tests
│ → Open references/writing-tests.md (use cases, coverage, renderer/preset, file structure)
│ → If test needs navigation: also open references/navigation-mocking.md
│ → After writing: run tests, then open references/reference.md for self-review
├─ Fix failing test
│ → Run: yarn jest -c jest.config.view.js <path> --runInBand --silent --coverage=false
│ → Identify error type → Open references/reference.md (Diagnosing Failures)
└─ Run tests or self-review after tests pass
→ Open references/reference.md (Run the Tests, Self-Review Checklist)
```

Do not read the full reference files until the decision tree or workflow sends you there.

---

## What Are Component View Tests?

Component view tests are **integration-level** tests that test views through real Redux state — no mocked hooks or selectors. They live alongside the component as `ComponentName.view.test.tsx` and use a dedicated framework in `tests/component-view/`.

Key constraint: **only Engine and allowed native modules may be mocked** (enforced at runtime by `app/util/test/testSetupView.js` and by ESLint override in `.eslintrc.js` for `**/*.view.test.*`).

---

## The Framework at a Glance

```
tests/component-view/
├── mocks.ts ← Engine + native mocks (import this first, always)
├── render.tsx ← renderComponentViewScreen, renderScreenWithRoutes
├── stateFixture.ts ← StateFixtureBuilder (createStateFixture)
├── presets/ ← initialState<Feature>() builders — one file per feature area
└── renderers/ ← render<Feature>View() functions — one file per feature area
```

---

## Workflow (summary)

- **Write new test**: Read component and existing tests → list use cases and map to test patterns → check coverage and deduplicate → use or create renderer/preset → write test (use `renderScreenWithRoutes` if asserting navigation). Every test must have at least one of: `fireEvent`, `waitFor`/`findBy`, `store.dispatch`/`act`, or Engine spy (no render-only scenarios). Run tests, then run the self-review checklist in `references/reference.md`.
- **Fix failing test**: Run with `jest.config.view.js` → identify error type from the table in `references/reference.md` (Diagnosing Failures) → apply the fix (remove disallowed mock, add state override, add preset, wrap in `waitFor`, add `deterministicFiat`, etc.) → re-run.
- **Update after change**: Same as write — review existing tests, extend preset/renderer if needed, update tests, run and self-review.

For full detail (use cases, coverage, presets, route probes, self-review checklist, failure table), use the reference files when the decision tree sends you there.

---

## Run the tests

Always use `jest.config.view.js` — the default Jest config does not apply component view test rules.

**Run tests (no coverage):**

```bash
yarn jest -c jest.config.view.js <path> --runInBand --silent --coverage=false
```

Example: `yarn jest -c jest.config.view.js app/components/UI/Bridge/Views/BridgeView/BridgeView.view.test.tsx --runInBand --silent --coverage=false`

**Coverage for a feature folder** (use this instead of `--coverage` to avoid OOM):

```bash
yarn test:view:coverage:folder app/components/UI/MyFeature
```

For run-by-name, watch mode, or other options, see `references/reference.md` (Run the Tests).

---

## Golden Rules (Enforced)

1. **Only mock Engine and allowed native modules** — no arbitrary `jest.mock()` in `*.view.test.*` files. Allowed:
- `../../app/core/Engine`
- `../../app/core/Engine/Engine`
- `react-native-device-info`
- (these are already handled by `tests/component-view/mocks.ts`)

2. **Drive all behavior through Redux state** — no mocking of hooks or selectors. Provide data via state overrides.

3. **Reuse presets and renderers** — never rebuild the full state manually from scratch.

4. **No fake timers** — never use `jest.useFakeTimers()`, `jest.advanceTimersByTime()`, or `jest.useRealTimers()`.

5. **Test behavior, not snapshots** — use `toBeOnTheScreen()`, `not.toBeOnTheScreen()`, interaction assertions.

6. **Follow AAA** — Arrange → Act → Assert, blank lines between each section. One test = one user journey or business outcome; multiple chained actions in a single test are fine.

7. **No render scenarios** — every test must have at least one of: `fireEvent`, `waitFor`/`findBy`, `store.dispatch`/`act`, or an Engine spy. Static visibility checks are not tests. See [`references/writing-tests.md`](references/writing-tests.md) for examples.

8. **Use selector ID constants, never raw strings** — every `getByTestId` / `findByTestId` / `queryByTestId` must reference a constant from `ComponentName.testIds.ts`. Create the file if it does not exist.

9. **Every view with async data needs one data-completeness test** — wait for the load and validate all significant fields of all items in the base mock using `within()` per row. One per independent async data flow.

10. **Filter / segmentation tests must assert both sides** — after selecting a filter, assert both what appears (positive `findByTestId`) and what disappears (negative `queryByTestId(...).not.toBeOnTheScreen()`).

---

## Reference files (when to use)

Documentation is split by **action**. Open only the reference that matches what you are doing.

| Action | File | When to open it |
| ----------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Writing or updating view tests** | [`references/writing-tests.md`](references/writing-tests.md) | New test file, new or updated preset/renderer. Read before writing, use cases and coverage, file structure, renderers, presets, route params. |
| **Testing navigation** | [`references/navigation-mocking.md`](references/navigation-mocking.md) | Route probes, single nav push, multi-screen renderer, cross-screen journey, external/API mocking. |
| **Running tests, self-review, fixing failures** | [`references/reference.md`](references/reference.md) | Run the Tests, Self-Review Checklist, Diagnosing Failures, assertion patterns, Deterministic Fiat Assertions, What NOT to Do, Quick Reference. |

**Where self-review and What NOT to Do live:** Both are in `references/reference.md`. Self-review is the checklist you run after tests pass. What NOT to Do is the antipatterns section in the same file. Keeping them there means when you run tests or fix failures you have run commands, the checklist, the failure table, and the antipatterns in one place — open that reference for any run/fix/review task.
4 changes: 4 additions & 0 deletions .agents/skills/component-view-test/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Component View Test"
short_description: "Write and fix *.view.test.tsx integration tests for MetaMask Mobile."
default_prompt: "Use $component-view-test to write, fix, or update component view tests with the tests/component-view/ framework."
235 changes: 235 additions & 0 deletions .agents/skills/component-view-test/references/navigation-mocking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# Navigation & Mocking

Reference: [SKILL.md](../SKILL.md) · [Writing Tests](writing-tests.md) · [Reference](reference.md)

---

## Navigation Testing

### How `renderScreenWithRoutes` works

`renderScreenWithRoutes(EntryComponent, entryRoute, routesArray, options)` — entry screen, its route name, an array of **1 to N** routes, and options (e.g. `{ state }`). Each route is `{ name: Routes.X }` or `{ name: Routes.X, Component: RealComponent }`.

When navigation hits a registered route, the framework renders an element with `` testID=`route-${routeName}` `` so you can assert with ``await findByTestId(`route-${Routes.X}`)``. If you passed `Component`, the real component is shown instead. Use only `{ name }` when you just need to assert navigation; use `Component` when the test interacts with the destination screen. For cross-screen journeys, use a renderer that registers all reachable routes with `Component`.

```typescript
// tests/component-view/renderers/myFeature.ts
export function renderMyFeatureWithRoutes(options = {}) {
const state = initialStateMyFeature(options).build();

return renderScreenWithRoutes(
MyFeatureHome as unknown as React.ComponentType,
{ name: Routes.MY_FEATURE.HOME },
[
{
name: Routes.MY_FEATURE.DETAIL,
Component: MyFeatureDetail as unknown as React.ComponentType<unknown>,
},
{
name: Routes.MY_FEATURE.FILTER_MODAL,
Component: MyFeatureFilter as unknown as React.ComponentType<unknown>,
},
{
name: Routes.MY_FEATURE.ASSET,
Component: AssetDetails as unknown as React.ComponentType<unknown>,
},
],
{ state },
);
}
```

### Cross-screen journey test

The most valuable navigation tests follow a **complete user journey across multiple screens**:

```typescript
// ✅ User navigates from feed → full list view, then applies a network filter —
// the list updates with chain-specific data.
//
// Key techniques in this pattern:
//
// 1. DYNAMIC MOCK — the mock responds differently based on the params the component
// passes. This proves the component sends the correct filter params to the API,
// not just that the UI reacts to props.
//
// 2. PAIRED ASSERTIONS — after selecting the filter, assert BOTH sides:
// - Positive: new items appear (the filtered set)
// - Negative: old items are gone (queryByTestId(...).not.toBeOnTheScreen())
// Asserting only one side does not prove the list actually changed.
it('displays only BNB tokens when BNB Chain network filter is selected', async () => {
// Dynamic mock: returns different data based on the chainIds param
getMyFeatureDataMock.mockImplementation(async (params) => {
if (params?.chainIds?.length === 1 && params.chainIds[0] === 'eip155:56') {
return mockBnbChainToken; // BNB-specific dataset
}
return mockTokensData; // default multi-chain dataset
});

const { findByTestId, findByText, getByTestId, queryByTestId } =
renderMyFeatureWithRoutes();

// Screen 1: wait for feed to load (confirms default data is visible)
await waitFor(() =>
expect(
getByTestId(MyFeatureSelectorsIDs.FEED_SCROLL_VIEW),
).toBeOnTheScreen(),
);

// Navigate to full list view
await userEvent.press(getByTestId('section-header-view-all'));
await waitFor(() =>
expect(getByTestId('full-list-header')).toBeOnTheScreen(),
);

// Confirm a non-BNB token is visible before filtering
expect(
await findByTestId('token-row-eip155:1/erc20:0xAAA...'),
).toBeOnTheScreen();

// Open network filter modal and select BNB Chain
await userEvent.press(getByTestId('all-networks-button'));
await waitFor(() => expect(getByTestId('close-button')).toBeOnTheScreen());
await userEvent.press(await findByText('BNB Chain'));

// ✅ Positive assertions — BNB token appears with all its fields
const bnbRow = await findByTestId('token-row-eip155:56/erc20:0xBTC000...');
expect(within(bnbRow).getByText('Bitcoin BNB')).toBeOnTheScreen();
expect(within(bnbRow).getByText(/\$44,500/)).toBeOnTheScreen();
expect(within(bnbRow).getByText(/-1\.8/)).toBeOnTheScreen();

// ✅ Negative assertions — previous chain tokens are gone (proves the list changed,
// not just that new items were added on top)
expect(
queryByTestId('token-row-eip155:1/erc20:0xAAA...'),
).not.toBeOnTheScreen();
expect(
queryByTestId('token-row-eip155:1/erc20:0xBBB...'),
).not.toBeOnTheScreen();
expect(
queryByTestId('token-row-eip155:1/erc20:0xCCC...'),
).not.toBeOnTheScreen();
});
```

### `userEvent` vs `fireEvent`

For interactions that involve realistic user behavior (typing, pressing with focus), prefer `userEvent` over `fireEvent`:

```typescript
import { fireEvent, userEvent } from '@testing-library/react-native';

// ✅ userEvent — simulates full event sequence including focus, pointer events
await userEvent.press(getByTestId('button'));
await userEvent.type(getByTestId('search-input'), 'ethereum');

// fireEvent — lower-level, useful when userEvent isn't available or for non-user events
fireEvent.press(getByTestId('button'));
fireEvent.changeText(getByTestId('input'), 'value');
```

Route names live in `app/constants/navigation/Routes.ts`.

---

## External Service / API Mocking

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.

### Current pattern — jest.mock on the service module

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:

```typescript
// tests/component-view/mocks/myFeatureApiMocks.ts
import { getMyFeatureData } from '@metamask/some-package';

export const getMyFeatureDataMock = getMyFeatureData as jest.Mock;

export const mockFeatureData = [
{ id: 'item-1', name: 'Token A', price: '100.00', change24h: 5.2 },
{ id: 'item-2', name: 'Token B', price: '200.00', change24h: -1.8 },
];

export const setupMyFeatureApiMock = (data = mockFeatureData) => {
getMyFeatureDataMock.mockImplementation(async () => data);
};

export const clearMyFeatureApiMocks = () => {
jest.clearAllMocks();
};
```

In the test file, declare the `jest.mock` at module scope and use `beforeEach`/`afterEach` for lifecycle:

```typescript
// NOTE: antipattern — only Engine and native modules should be mocked in view tests.
// This is a temporary workaround for service functions called directly from components,
// not through Engine. Track removal in the linked issue.
// eslint-disable-next-line no-restricted-syntax
jest.mock('@metamask/some-package', () => {
const actual = jest.requireActual('@metamask/some-package');
return { ...actual, getMyFeatureData: jest.fn().mockResolvedValue([]) };
});

import {
setupMyFeatureApiMock,
clearMyFeatureApiMocks,
mockFeatureData,
getMyFeatureDataMock,
} from '../../../../tests/component-view/mocks/myFeatureApiMocks';

describe('MyFeatureView', () => {
beforeEach(() => {
setupMyFeatureApiMock(mockFeatureData);
});

afterEach(() => {
clearMyFeatureApiMocks();
});

it('shows token list after data loads from the external service', async () => {
const { findByText } = renderMyFeatureWithRoutes();

expect(await findByText('Token A')).toBeOnTheScreen();
});

it('shows only filtered results when a specific param is passed', async () => {
getMyFeatureDataMock.mockImplementation(async (params) => {
if (params?.chainId === 'eip155:56') return [mockBnbData];
return mockFeatureData;
});

const { findByText } = renderMyFeatureWithRoutes();
// ... interact to trigger the filter, then assert
});
});
```

> ⚠️ **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.

### Future pattern — Mock Service Worker (MSW)

> 📌 **Placeholder — no example exists yet in this codebase.**

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.

When the first MSW-based view test is written, document the setup here:

```typescript
// TODO: Add MSW setup example once the first test using it is merged.
// Expected shape:
//
// import { setupServer } from 'msw/node';
// import { http, HttpResponse } from 'msw';
//
// const server = setupServer(
// http.get('https://api.example.com/tokens', () =>
// HttpResponse.json(mockTokensData),
// ),
// );
//
// beforeAll(() => server.listen());
// afterEach(() => server.resetHandlers());
// afterAll(() => server.close());
```
Loading
Loading