From 7728f69647bebeceba68c4e34637ebab05e786d3 Mon Sep 17 00:00:00 2001 From: Maggie Date: Tue, 3 Mar 2026 13:46:16 +0200 Subject: [PATCH 1/2] docs: add feature flags in E2E tests section --- docs/testing/e2e-testing.md | 158 ++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/docs/testing/e2e-testing.md b/docs/testing/e2e-testing.md index df786891..c0e54140 100644 --- a/docs/testing/e2e-testing.md +++ b/docs/testing/e2e-testing.md @@ -206,3 +206,161 @@ solution: replace UI steps that build up the application state with the FixtureB scenario: import Account using private key and remove imported account solution: replace UI steps that build up the application state with the FixtureBuilder ``` + +## Feature flags in E2E tests + +MetaMask uses two categories of feature flags, and each requires a different approach in E2E tests. Understanding the distinction helps you write tests that accurately reflect production behavior. + +### Remote feature flags (runtime) + +Remote feature flags are fetched at runtime from a configuration service. In production, the application calls a remote API to retrieve flag values. During E2E tests, a mock server or fixture intercepts these requests and returns controlled values instead. + +Each client should maintain a **feature flag registry** — a central source of truth that maps every remote flag to its current production default value. The E2E mock layer reads from this registry so that tests run against production-accurate defaults by default, without calling the real API. + +#### Guidelines + +- **Default to production values.** Tests should use the same flag values that real users see unless the test is specifically verifying behavior behind a different flag state. The registry makes this automatic. +- **Override only when needed.** When a test must exercise a non-default flag state, use the test framework's override mechanism (e.g. fixture builder methods or manifest flag overrides) rather than changing the registry itself. +- **Register every flag.** Any remote flag referenced in application code should have a corresponding entry in the registry. CI checks can enforce this automatically by scanning for flag references and verifying they exist in the registry. +- **Keep the registry up to date.** When a flag is fully rolled out or removed from the remote API, update or remove its registry entry. Stale entries lead to tests that no longer reflect production. +- **No custom builds required.** Because remote flags are resolved at runtime, you do not need to create a special build to test different remote flag values. Override them at the test level. + +#### Examples + +✅ Registry entry with production default — tests automatically use this value: + +```typescript +redesignedConfirmations: { + name: 'redesignedConfirmations', + type: FeatureFlagType.Remote, + productionDefault: true, + status: FeatureFlagStatus.Active, +}, +``` + +✅ Override a remote flag for a specific test without changing the registry: + +```javascript +// Override via fixture builder +new FixtureBuilder() + .withRemoteFeatureFlags({ redesignedConfirmations: false }) + .build(); +``` + +```javascript +// Override via manifest flags in test options +await withFixtures( + { + manifestFlags: { + remoteFeatureFlags: { redesignedConfirmations: false }, + }, + }, + async () => { + // test runs with the flag disabled + }, +); +``` + +❌ Modifying the registry to change a flag value for a single test: + +```typescript +// DON'T do this — it changes the default for all tests +redesignedConfirmations: { + name: 'redesignedConfirmations', + productionDefault: false, // changed from true just for one test +}, +``` + +### Build-time feature flags (compile-time) + +Build-time feature flags are set during the build process and baked into the compiled output. They control which code paths are included in a given build. Changing a build-time flag requires creating a new build before running tests. + +#### Guidelines + +- **Create a dedicated test build.** To test with a build-time flag enabled, set the flag in the build configuration or pass it as an environment variable, then create a test build. +- **Keep build-time flags separate from remote flags.** Do not conflate the two. A build-time flag controls what code ships; a remote flag controls runtime behavior of code that is already shipped. +- **Document available flags.** Each client should document its build-time flags and how to enable them for test builds, so contributors know which flags exist and how to use them. + +#### Examples + +✅ Enable a build-time flag via environment variable, then run tests: + +```bash +# Create a test build with the flag enabled +MULTICHAIN=1 yarn build:test + +# Run E2E tests against that build +yarn test:e2e +``` + +✅ Enable a build-time flag via local configuration file: + +```bash +# In your local config file (e.g. .metamaskrc, .env, etc.) +MULTICHAIN=1 + +# Then build and test as usual +yarn build:test +yarn test:e2e +``` + +❌ Trying to override a build-time flag at the test level (this has no effect): + +```javascript +// DON'T do this — build-time flags are already compiled in +new FixtureBuilder() + .withBuildFlag({ MULTICHAIN: true }) // has no effect at runtime + .build(); +``` + +### General principles + +- **Test both states when possible.** For any flag that gates significant user-facing behavior, consider having tests for both the enabled and disabled states to prevent regressions in either path. +- **Avoid flag-dependent test logic in shared helpers.** If a helper function behaves differently based on a flag, make the flag value an explicit parameter rather than reading it implicitly. This keeps tests predictable and easy to reason about. +- **Clean up after rollout.** Once a feature flag is no longer needed (the feature is fully launched or removed), delete the flag references from application code, tests, and the registry. Leftover flags add confusion and maintenance burden. + +#### Examples + +✅ Testing both flag states explicitly: + +```javascript +describe('token approvals', () => { + it('shows redesigned confirmation when flag is enabled', async () => { + new FixtureBuilder() + .withRemoteFeatureFlags({ redesignedConfirmations: true }) + .build(); + // assert redesigned UI is shown + }); + + it('shows legacy confirmation when flag is disabled', async () => { + new FixtureBuilder() + .withRemoteFeatureFlags({ redesignedConfirmations: false }) + .build(); + // assert legacy UI is shown + }); +}); +``` + +✅ Making flag dependency explicit in a helper: + +```javascript +function confirmTransaction(driver, { isRedesigned }) { + if (isRedesigned) { + return driver.clickElement('[data-testid="confirm-redesigned"]'); + } + return driver.clickElement('[data-testid="confirm-legacy"]'); +} +``` + +❌ Reading the flag implicitly inside a shared helper: + +```javascript +// DON'T do this — the helper's behavior depends on hidden global state +function confirmTransaction(driver) { + const flags = getRemoteFeatureFlags(); + if (flags.redesignedConfirmations) { + return driver.clickElement('[data-testid="confirm-redesigned"]'); + } + return driver.clickElement('[data-testid="confirm-legacy"]'); +} +``` From f7f987e116fa669532603b778f70e8bdc0d725cb Mon Sep 17 00:00:00 2001 From: Maggie Date: Wed, 4 Mar 2026 17:29:24 +0200 Subject: [PATCH 2/2] docs: replace E2E test examples with cross-reference to e2e-testing.md --- docs/remote-feature-flags.md | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/docs/remote-feature-flags.md b/docs/remote-feature-flags.md index 04bb8733..47181f94 100644 --- a/docs/remote-feature-flags.md +++ b/docs/remote-feature-flags.md @@ -223,6 +223,8 @@ Your selector must include: #### Extension +In production, MetaMask Extension fetches remote flags from the [client-config API](https://client-config.api.cx.metamask.io/v1/flags?client=extension&distribution=main&environment=prod) at runtime. During E2E tests, a global mock server (`test/e2e/mock-e2e.js`) reads from the [feature flag registry](https://github.com/MetaMask/metamask-extension/blob/main/test/e2e/feature-flags/feature-flag-registry.ts) instead of calling the real API. Each registry entry stores the flag's production default value, so tests reflect real-world behavior unless a specific test explicitly overrides a flag. + ##### Local Feature Flag Override - Developers can override `remoteFeatureFlag` values by defining them in `.manifest-overrides.json` and enable `MANIFEST_OVERRIDES=.manifest-overrides.json` in the `.metamaskrc.dist` locally. @@ -259,22 +261,7 @@ Your selector must include: ##### B. E2E Test -Add the customized value in your test configuration: - -```typescript -await withFixtures({ - fixtures: new FixtureBuilder() - .withMetaMetricsController({ - metaMetricsId: MOCK_META_METRICS_ID, - participateInMetaMetrics: true, - }) - .build(), - manifestFlags: { - remoteFeatureFlags: MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS, - }, - title: this.test?.fullTitle(), -}); -``` +For detailed guidelines on handling remote (and build-time) feature flags in E2E tests — including the feature flag registry, override patterns, and general principles — see [Feature flags in E2E tests](testing/e2e-testing.md#feature-flags-in-e2e-tests). #### Mobile