Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bf69cd8
test(e2e): harden dapp-interactions tab selection and add stress work…
davidmurdoch Feb 28, 2026
a92d2d8
ci: skip standard e2e workflows on stress-test branch
davidmurdoch Feb 28, 2026
02243db
Fix stress workflow shell for pipefail
davidmurdoch Feb 28, 2026
d8a504c
Harden dapp interactions account connection assertion
davidmurdoch Feb 28, 2026
4a099fb
Remove flaky dapp-side account assertion
davidmurdoch Feb 28, 2026
93c049e
Retry header menu click on interception
davidmurdoch Mar 1, 2026
48008fc
Increase wait for connected sites in dapp interactions
davidmurdoch Mar 1, 2026
a49dff1
Retry second dapp connect when account request fails
davidmurdoch Mar 1, 2026
1cc7106
fix(eip1193): dedupe concurrent eth_requestAccounts per origin
davidmurdoch Mar 1, 2026
97a50ae
fix(ui): prevent unread badge from intercepting menu clicks
davidmurdoch Mar 1, 2026
2ea2c72
fix(permissions): await approval submission before redirect
davidmurdoch Mar 1, 2026
7cc45fc
fix(eip1193): retry account lookup after permissions approval
davidmurdoch Mar 1, 2026
6a607bb
fix(eip1193): fall back to granted accounts on connect
davidmurdoch Mar 1, 2026
01f1d98
fix(permissions): avoid stale redirect while approval is pending
davidmurdoch Mar 1, 2026
8d4cf02
fix(multichain-connect): stabilize derived account selection during a…
davidmurdoch Mar 1, 2026
2cb6290
fix(multichain-connect): default eip1193 requests to evm scopes
davidmurdoch Mar 1, 2026
17656b8
fix(permissions): restore approval routing and multichain defaults
davidmurdoch Mar 1, 2026
da04cdb
fix(request-accounts): tighten CAIP-25 value null guard
davidmurdoch Mar 1, 2026
49fd60c
fix(permissions): await approvals with guarded redirect routing
davidmurdoch Mar 1, 2026
495b71d
fix(connect): derive caip account payload at confirm time
davidmurdoch Mar 1, 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
90 changes: 90 additions & 0 deletions .github/workflows/e2e-dapp-interactions-stress.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: E2E Dapp Interactions Stress

on:
pull_request:
paths:
- '.github/workflows/e2e-dapp-interactions-stress.yml'
- 'test/e2e/tests/dapp-interactions/dapp-interactions.spec.ts'
- 'test/e2e/run-e2e-test.js'
workflow_dispatch:
inputs:
attempts:
description: Number of repeated attempts for this spec
required: false
default: '30'

jobs:
test-e2e-dapp-interactions-stress:
name: test-e2e-dapp-interactions-stress
runs-on: ubuntu-latest
timeout-minutes: 80
container:
image: ghcr.io/metamask/metamask-extension-e2e-image:v24.13.0
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INFURA_PROJECT_ID: ${{ secrets.INFURA_PROJECT_ID }}
SENTRY_DSN_PERFORMANCE: ${{ vars.SENTRY_DSN_PERFORMANCE }}
FORCE_COLOR: '1'
ATTEMPTS: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.attempts || '30' }}
RUN_ID: ${{ github.run_id }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}

steps:
- name: Checkout and setup environment
uses: MetaMask/action-checkout-and-setup@v3
with:
is-high-risk-environment: false
skip-allow-scripts: true

- name: Restore .metamask folder
id: restore-metamask
uses: actions/cache/restore@v5
with:
path: .metamask
key: .metamask-${{ hashFiles('yarn.lock') }}
fail-on-cache-miss: false

- name: Install anvil if cache missed
if: ${{ steps.restore-metamask.outputs.cache-hit != 'true' }}
run: yarn mm-foundryup

- name: Build test artifacts
run: yarn build:test

- name: Configure Xvfb
run: Xvfb -ac :99 -screen 0 1280x1024x16 &

- name: Run dapp interactions stress test
env:
PROPERTIES: JOB_NAME:test-e2e-dapp-interactions-stress,RUN_ID:${{ env.RUN_ID }},PR_NUMBER:${{ env.PR_NUMBER }}
shell: bash
run: |
set -euo pipefail

if ! [[ "${ATTEMPTS}" =~ ^[0-9]+$ ]] || [ "${ATTEMPTS}" -lt 1 ]; then
echo "Invalid ATTEMPTS value: ${ATTEMPTS}"
exit 1
fi

RETRIES=$((ATTEMPTS - 1))

echo "Running test/e2e/tests/dapp-interactions/dapp-interactions.spec.ts ${ATTEMPTS} times"

yarn test:e2e:single \
test/e2e/tests/dapp-interactions/dapp-interactions.spec.ts \
--browser chrome \
--stop-after-one-failure \
--retries "${RETRIES}" \
--debug=false

- name: Upload test artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: test-e2e-dapp-interactions-stress
path: |
./test-artifacts
./test/test-results/e2e
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import type {
JsonRpcRequest,
PendingJsonRpcResponse,
} from '@metamask/utils';
import {
Caip25CaveatType,
Caip25EndowmentPermissionName,
} from '@metamask/chain-agnostic-permission';
import * as Util from '../../util';
import requestEthereumAccounts from './request-accounts';

Expand Down Expand Up @@ -50,7 +54,7 @@ const createMockedHandler = () => {
const getCaip25PermissionFromLegacyPermissionsForOrigin = jest
.fn()
.mockResolvedValue({});
const requestPermissionsForOrigin = jest.fn().mockReturnValue({});
const requestPermissionsForOrigin = jest.fn().mockResolvedValue([{}]);
const response: PendingJsonRpcResponse<string[]> = {
jsonrpc: '2.0' as const,
id: 0,
Expand Down Expand Up @@ -150,6 +154,123 @@ describe('requestEthereumAccountsHandler', () => {
expect(getAccounts).toHaveBeenCalledTimes(2);
});

it('briefly retries account lookup if approval result has not propagated yet', async () => {
const { handler, getAccounts, response } = createMockedHandler();
getAccounts
.mockReturnValueOnce([])
.mockReturnValueOnce([])
.mockReturnValueOnce(['0xdead']);

await handler(baseRequest);

expect(response.result).toStrictEqual(['0xdead']);
expect(getAccounts).toHaveBeenCalledTimes(3);
});

it('falls back to granted CAIP-25 accounts when account lookup remains empty', async () => {
const { handler, getAccounts, response, requestPermissionsForOrigin } =
createMockedHandler();

getAccounts.mockReturnValue([]);
requestPermissionsForOrigin.mockResolvedValue([
{
[Caip25EndowmentPermissionName]: {
caveats: [
{
type: Caip25CaveatType,
value: {
isMultichainOrigin: true,
requiredScopes: {},
optionalScopes: {
'eip155:1': {
accounts: ['eip155:1:0xdead'],
},
},
sessionProperties: {},
},
},
],
},
},
]);

await handler(baseRequest);

expect(response.result).toStrictEqual(['0xdead']);
});

it('shares the same in-flight request for concurrent requests from the same origin', async () => {
const {
next,
getAccounts,
sendMetrics,
metamaskState,
getCaip25PermissionFromLegacyPermissionsForOrigin,
requestPermissionsForOrigin,
} = createMockedHandler();

let resolveApprovalRequest: (() => void) | undefined;
const approvalRequestPromise = new Promise<void>((resolve) => {
resolveApprovalRequest = resolve;
});
requestPermissionsForOrigin.mockImplementation(async () => {
await approvalRequestPromise;
return [{}];
});
getAccounts.mockReturnValueOnce([]).mockReturnValueOnce(['0xdead']);

const firstResponse: PendingJsonRpcResponse<string[]> = {
jsonrpc: '2.0',
id: 1,
result: undefined,
};
const secondResponse: PendingJsonRpcResponse<string[]> = {
jsonrpc: '2.0',
id: 2,
result: undefined,
};
const firstEnd = jest.fn();
const secondEnd = jest.fn();

const firstRequestPromise = requestEthereumAccounts.implementation(
{ ...baseRequest, id: 1 },
firstResponse,
next,
firstEnd,
{
getAccounts,
sendMetrics,
metamaskState,
getCaip25PermissionFromLegacyPermissionsForOrigin,
requestPermissionsForOrigin,
},
);
const secondRequestPromise = requestEthereumAccounts.implementation(
{ ...baseRequest, id: 2 },
secondResponse,
next,
secondEnd,
{
getAccounts,
sendMetrics,
metamaskState,
getCaip25PermissionFromLegacyPermissionsForOrigin,
requestPermissionsForOrigin,
},
);

expect(requestPermissionsForOrigin).toHaveBeenCalledTimes(1);

resolveApprovalRequest?.();
await Promise.all([firstRequestPromise, secondRequestPromise]);

expect(firstResponse.result).toStrictEqual(['0xdead']);
expect(secondResponse.result).toStrictEqual(['0xdead']);
expect(getAccounts).toHaveBeenCalledTimes(2);
expect(firstEnd).toHaveBeenCalled();
expect(secondEnd).toHaveBeenCalled();
});

it('emits the dapp viewed metrics event when shouldEmitDappViewedEvent returns true', async () => {
const { handler, getAccounts, sendMetrics } = createMockedHandler();
getAccounts
Expand Down
Loading
Loading