| name | performance-testing |
| description | Create and review E2E performance tests that measure real user flows on real devices with TimerHelper and PerformanceTracker. Use when creating, editing, or reviewing performance tests, when the user mentions perf tests, timing measurements, performance thresholds, or files in tests/performance/. |
E2E Performance Testing
For the full reference guide with templates, examples, and decision trees, read reference.md.
Core Rules
- Real device, real app, real network — zero mocking
- Actions OUTSIDE
measure(), assertions INSIDE measure() — measure app response time, not interactions
- One
TimerHelper per measurable step with platform-specific thresholds { ios: <ms>, android: <ms> }
- User-centric timer descriptions: "Time since the user clicks X until Y is visible"
- Screen Object pattern — all UI via
wdio/screen-objects/, never raw selectors
- Every screen object must have
device assigned before use
- Every test must call
performanceTracker.addTimers() + performanceTracker.attachToTest(testInfo)
- Performance + team tags are mandatory on every test
File Location
| Starting condition | Folder |
|---|
| User already has wallet (login screen) | tests/performance/login/ |
| Fresh install (onboarding) | tests/performance/onboarding/ |
| Dapp connection needed | tests/performance/mm-connect/ |
Quick Template (login-based)
import { test } from '../../framework/fixtures/performance';
import TimerHelper from '../../framework/TimerHelper';
import { login } from '../../framework/utils/Flows.js';
import { PerformanceLogin } from '../../tags.performance.js';
test.describe(`${PerformanceLogin}`, () => {
test(
'Descriptive name',
{ tag: '@team-name' },
async ({ device, performanceTracker }, testInfo) => {
ScreenA.device = device;
ScreenB.device = device;
await login(device);
const timer = new TimerHelper(
'Time since the user clicks X until Y is visible',
{ ios: 2000, android: 3000 },
device,
);
await ScreenA.tapButton();
await timer.measure(() => ScreenB.isVisible());
performanceTracker.addTimers(timer);
await performanceTracker.attachToTest(testInfo);
},
);
});
Modifying Existing Tests
When editing an existing test in tests/performance/, follow these rules:
Thresholds
- Never tighten thresholds without baseline data — run the test 3+ times first to collect real timings
- Never remove thresholds — every timer must keep
{ ios, android } values
- Document why in the PR if changing a threshold (e.g., "tightened after 10 runs averaging 1.2s")
- If a test is consistently failing quality gates, widen the threshold rather than deleting the timer
Adding/Removing Timers
- When adding a new timer to an existing test, register it in the existing
performanceTracker.addTimers() call
- When removing a timer, ensure it's also removed from
addTimers() — orphaned timers cause silent failures
- Never reduce a test to zero timers — if the flow no longer needs measurement, delete the test file
Screen Object Changes
- If a screen object method is renamed or removed, grep all
tests/performance/ for usages before changing
- When adding methods to a screen object in
wdio/screen-objects/, ensure backward compatibility — existing tests must not break
- New screen objects must follow the same
device getter/setter pattern
UI Flow Changes
- If the app UI changes (new screen, removed step, renamed button), update all perf tests that use that flow
- Re-run the affected tests to verify timers still measure the intended transition
- Update timer descriptions if the user-facing flow changed (e.g., button was renamed)
Refactoring
- Extract shared flows into
tests/framework/utils/Flows.js (e.g., login, importSRPFlow)
- If multiple tests duplicate the same setup, create a helper that returns
TimerHelper[]
- Keep
screensSetup(device) pattern for tests with many screen objects (see perps-position-management.spec.js)
Code Review Checklist for Modifications
Forbidden Patterns
jest.mock(...) — no mocking
import { test } from 'appwright' — always from fixtures/performance
- Actions inside
measure() — only assertions/waits
- Missing
device assignment on screen objects
- Timers without thresholds
- Missing team tag
- Hardcoded passwords — use
getPasswordForScenario()
Threshold Ranges
| Action type | iOS | Android |
|---|
| Simple screen transition | 500–1500 ms | 600–1800 ms |
| Data loading (API + render) | 1500–5000 ms | 2000–7000 ms |
| Dapp connection (cross-context) | 8000–20000 ms | 12000–30000 ms |
| Quote/swap execution | 7000–9000 ms | 7000–9000 ms |
Key References
- Full guide: reference.md
- Performance fixture:
tests/framework/fixtures/performance/performance-fixture.ts
- TimerHelper:
tests/framework/TimerHelper.ts
- Tags:
tests/tags.performance.js
- Flows:
tests/framework/utils/Flows.js
- Screen objects:
wdio/screen-objects/
- Examples:
tests/performance/login/, tests/performance/onboarding/