Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4af9613
test: e2e cross-chain swaps
shoom3301 Aug 11, 2026
b997fa0
Merge branch 'e2e/market-orders' of https://github.com/cowprotocol/co…
shoom3301 Aug 11, 2026
388e897
docs: update e2e AGENTS.md
shoom3301 Aug 11, 2026
7f8cd6e
Merge branch 'e2e/market-orders' of https://github.com/cowprotocol/co…
shoom3301 Aug 12, 2026
4fcdd1a
Merge branch 'e2e/market-orders' of https://github.com/cowprotocol/co…
shoom3301 Aug 12, 2026
24d23ce
test(e2e): mock all rpc node requests
shoom3301 Aug 12, 2026
8523c86
Merge branch 'e2e/market-orders' of https://github.com/cowprotocol/co…
shoom3301 Aug 12, 2026
9e31d9f
Merge branch 'e2e/market-orders' of https://github.com/cowprotocol/co…
shoom3301 Aug 12, 2026
bbacd5f
chore: merge changes
shoom3301 Aug 12, 2026
b4b35a9
Merge branch 'e2e/market-orders' of https://github.com/cowprotocol/co…
shoom3301 Aug 12, 2026
8e90a15
Merge branch 'e2e/market-orders' of https://github.com/cowprotocol/co…
shoom3301 Aug 12, 2026
a45006b
chore: improve recoverDepositAddress hack
shoom3301 Aug 12, 2026
b91870f
chore: improve code
shoom3301 Aug 12, 2026
a273179
chore: improve tests stability
shoom3301 Aug 12, 2026
bdd7e39
chore: improve tests stability
shoom3301 Aug 13, 2026
f9b0726
chore: update test ids
shoom3301 Aug 13, 2026
8b083e4
chore: add smoke tags
shoom3301 Aug 13, 2026
5e7b4ba
docs(e2e): update readme
shoom3301 Aug 13, 2026
ba42d68
Merge branch 'e2e/market-orders' of https://github.com/cowprotocol/co…
shoom3301 Aug 13, 2026
db9e09b
chore: fix mockSocketVerifier
shoom3301 Aug 13, 2026
1af51b3
chore: update docs
shoom3301 Aug 13, 2026
7aa1942
chore: fix workers count
shoom3301 Aug 13, 2026
29a00d0
chore: revert config
shoom3301 Aug 13, 2026
3f00b97
chore: improve selectors
shoom3301 Aug 13, 2026
403d1c0
chore: fix BridgeRoutePanel.expand
shoom3301 Aug 13, 2026
115609d
chore: fix config
shoom3301 Aug 13, 2026
b8372f9
chore: mock token nonce call
shoom3301 Aug 13, 2026
01bb3f1
chore: fix test
shoom3301 Aug 13, 2026
9e4bcc4
chore: adjust config
shoom3301 Aug 13, 2026
4f0cae1
feat(e2e): add OrdersMock, a multi-order registry replacing the singl…
shoom3301 Aug 13, 2026
999afd9
chore: fix config
shoom3301 Aug 13, 2026
de8060e
feat(e2e): add seedOpenOrder/wasCancelRequested/markCancelled to Orde…
shoom3301 Aug 13, 2026
415fb18
feat(e2e): add trackEthFlowOrder to OrdersMock, completing the merged…
shoom3301 Aug 13, 2026
6e02a5b
feat(e2e): wire OrdersMock into the mocks fixture as mocks.orders
shoom3301 Aug 13, 2026
fff52c0
refactor(e2e): migrate market-orders.spec.ts to mocks.orders
shoom3301 Aug 13, 2026
b79eaa1
fix(e2e): restore tradePage destructuring in market-orders.spec.ts (T…
shoom3301 Aug 13, 2026
f68596d
refactor(e2e): migrate cross-chain-swaps.spec.ts to mocks.orders
shoom3301 Aug 13, 2026
90cd46b
refactor(e2e): migrate limit-orders.spec.ts to mocks.orders
shoom3301 Aug 13, 2026
68ffe34
refactor(e2e): remove old order mocks
shoom3301 Aug 13, 2026
aa3351d
Merge remote-tracking branch 'origin/e2e/cross-chain-swaps' into e2e/…
shoom3301 Aug 13, 2026
08d4834
chore: bump workers
shoom3301 Aug 13, 2026
c66d30e
chore: remove tokens mock
shoom3301 Aug 13, 2026
513a336
chore: fix socketVerifier mock
shoom3301 Aug 13, 2026
36d390d
chore: fix tooltip
shoom3301 Aug 13, 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
1 change: 1 addition & 0 deletions .github/workflows/e2e-pw-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
env:
INTEGRATION_TEST_PRIVATE_KEY: ${{ secrets.INTEGRATION_TEST_PRIVATE_KEY }}
REACT_APP_NETWORK_URL_11155111: ${{ secrets.REACT_APP_NETWORK_URL_11155111 }}
REACT_APP_NETWORK_URL_1: ${{ secrets.REACT_APP_NETWORK_URL_1 }}
E2E_PW_MM_SEED: ${{ secrets.E2E_PW_MM_SEED }}
CI: 'true'
steps:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/e2e-pw-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
env:
INTEGRATION_TEST_PRIVATE_KEY: ${{ secrets.INTEGRATION_TEST_PRIVATE_KEY }}
REACT_APP_NETWORK_URL_11155111: ${{ secrets.REACT_APP_NETWORK_URL_11155111 }}
REACT_APP_NETWORK_URL_1: ${{ secrets.REACT_APP_NETWORK_URL_1 }}
E2E_PW_MM_SEED: ${{ secrets.E2E_PW_MM_SEED }}
CI: 'true'
steps:
Expand Down
1 change: 1 addition & 0 deletions apps/cowswap-e2e-tests/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
REACT_APP_NETWORK_URL_11155111=https://ethereum-sepolia-rpc.publicnode.com
REACT_APP_NETWORK_URL_1=https://ethereum-rpc.publicnode.com
INTEGRATION_TEST_PRIVATE_KEY=0x000000
96 changes: 95 additions & 1 deletion apps/cowswap-e2e-tests/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
author: agents
status: normative
last_reviewed: 2026-08-04
last_reviewed: 2026-08-11
source_of_truth_scope: cowswap-e2e-tests app-specific conventions, mocks, and debugging notes
---

Expand Down Expand Up @@ -125,6 +125,100 @@ behavior, only to call its methods). Sub-mocks:
`REACT_APP_NETWORK_URL_11155111` in its own environment (it's a separate process from the test runner,
which has it), or a transient DNS blip in the sandbox.

## Cross-chain bridging (`cross-chain-swaps.spec.ts`)

- **LaunchDarkly can't be mocked via HTTP here.** With no `REACT_APP_LAUNCH_DARKLY_KEY` configured,
the real LD SDK never even attempts flag-evaluation polling (only a `/sdk/goals/` call fires, never
`/sdk/evalx/...`), so route-mocking its API is a dead end. Instead `useFeatureFlags()`
(`libs/common-hooks/src/useFeatureFlags.ts`) reads `window.__COWSWAP_E2E_FEATURE_FLAGS__` directly and
merges it over the real (permanently unresolved) flags; `mocks/launchDarkly.ts` sets that window
property via `context.addInitScript`, and `mocks.launchDarkly.setFlag(key, value)` is how a spec turns
on `isBungeeBridgeProviderEnabled` / `isNearIntentsBridgeProviderEnabled` / `isSolBridgeEnabled` /
`isBtcBridgeEnabled`, etc. Bungee alone doesn't need this — it's added to the provider set
unconditionally at module load in `tradingSdk/bridgingSdk.ts`.
- **Near Intents' attestation is a real ECDSA signature check and cannot be forged.**
`recoverDepositAddress` verifies the quote/attestation pair against Near's real attestor key — a
captured fixture pair only satisfies it if replayed byte-for-byte for the exact route it was captured
for. `bridgingSdk.ts` patches `nearIntentsBridgeProvider.recoverDepositAddress` to a no-op success,
gated behind the existing `window.__COWSWAP_E2E__` flag — a production-source-file edit, but scoped to
e2e only. Consequently the Near fixture (`mocks/bridge/fixtures/near-quote.json` /
`near-attestation.json`) can only be served verbatim for the one route it was recorded against
(Mainnet USDC → Base USDC) — don't edit its numbers.
- **`BridgingSdk.getBestQuote()` always fetches a *regular* CoW quote first** (swap leg: sell token →
intermediate token) and feeds that quote's `buyAmount` in as the amount the bridge provider itself
quotes. The default `/quote` fixture's scaling is tuned for a same-decimals WETH:testUSDC pair and
produces nonsense for any other pair — always pin the swap leg with `mockFixedRateQuote` for a
cross-chain test. **When sell and intermediate-buy token decimals differ (e.g. native ETH's 18dec sell
→ a 6dec USDC intermediate), `mockFixedRateQuote`'s plain `sellAmount * numerator / denominator` is
decimals-*agnostic* and silently produces an amount ~12 orders of magnitude too large** (surfaces as
an absurd `"for at least 99.339B USDC"` in the confirm modal). Override `quote` a second time after
`mockFixedRateQuote` with a manually decimals-adjusted ratio in that case (see `[CC-13]`).
- **The app's own real-RPC traffic for a given chain does *not* reliably go through
`REACT_APP_NETWORK_URL_<chainId>`.** That env var only backs this suite's own wallet-side
dispatch/proxy (`walletEngine.ts` → `rpcProxy.ts`) and the handful of reads `mockEthFlowTransaction`/
`mockSocketVerifier` intercept by that exact URL (tx receipts, native-balance multicalls). Plenty of
other calls the *app itself* makes — Bungee's on-chain SocketVerifier check, `eth_estimateGas` before
every `eth_sendTransaction` — go straight to whichever of the app's own hardcoded providers it picks
(Infura, the WalletConnect RPC relay, publicnode, ...), unpredictable and outside this env var's
control. The only reliable way to intercept these is host-agnostic: `context.route('**/*', ...)`,
decode the JSON-RPC body, and match by `method` (see `mockSocketVerifier.ts` and
`mockEthEstimateGas` in `mockEthFlowTransaction.ts`), never by URL.
- **A real native-ETH sell (`[CC-13]`, eth-flow) needs `eth_estimateGas` stubbed too, not just
`eth_sendTransaction`.** Left unmocked, gas estimation is a real simulation against the wallet's real
on-chain balance — zero on Mainnet, since this is a shared test key with no real funds (never fund it;
Sepolia's equivalent test works only because that address genuinely holds real, free Sepolia ETH) — and
fails with a genuine "exceeds the balance of the account" error before the stubbed send is ever
reached.
- **`mockOrderPosting` doesn't work for eth-flow orders** — there's no `postOrder` call to hook (the uid
is computed client-side before anything is sent on-chain). Override `order`/`orderStatus` manually
instead (mirrors `[MO-11]`). One extra step specific to *bridging* eth-flow orders:
`useSwapAndBridgeContext` resolves the bridge provider from `order.apiAdditionalInfo.fullAppData`
(`bridgingSdk.getProviderFromAppData`) — without it, `bridgingStatus` never resolves and the progress
modal sticks on "Executing" forever regardless of what `order`/`orderStatus` say. Since an eth-flow tx
only carries the app-data *hash* on-chain (no room for the full JSON in a `bytes32`), capture the real
document via a `putAppData` override (`(req.body as { fullAppData: string }).fullAppData`) and thread
it into the `order` override's own `fullAppData` field.
- **"Expected to receive" and "Min. to receive" are computed completely differently for a bridge leg,
and only one of them gets rescaled to match the swap leg.** `useEstimatedBridgeBuyAmount` rescales the
swap leg's real output through the bridge quote's own before-fee ratio, so form `Receive (incl. fees)`,
the *bridge* stop's `Expected to receive`, and (for Bungee, whose mock scales proportionally) roughly
the swap stop's own figure all end up self-consistent. `Min. to receive` at the bridge stop is **not**
rescaled — it's the bridge SDK quote's raw `amountsAndCosts.afterSlippage.buyAmount`, carrying that
provider's own real routeFee/slippage. Don't assert equality between a swap leg's and a bridge leg's
`Min. to receive` — assert presence instead. For Near Intents specifically, both the quote's `sellAmount`
and `buyAmount` come from the same static signed fixture, so its bridge-stop `Min. to receive` is an
absolute number from that fixture, unrelated to whatever amount the test actually trades.
- **Solana availability needs two independent flags, Bitcoin needs only one.** `isSolBridgeEnabled` /
`isBtcBridgeEnabled` (the LD-bypass flags above) gate chain *availability* in
`useSupportedTargetChains`, but Solana additionally needs `IS_SOLANA_ENABLED` — a plain
`localStorage.getItem('IS_SOLANA_ENABLED')` check (`libs/common-const/src/featureFlags.ts`), a
completely different mechanism — for `CHAIN_INFO` to have a Solana entry to look up at all. Set it via
`context.addInitScript(() => localStorage.setItem('IS_SOLANA_ENABLED', '1'))` before navigating.
- **Near Intents' real dest-tokens fixture has no usable exact-"BTC" entry.** Its one `blockchain: "btc"`
token with `symbol: "BTC"` (`nep141:btc.omft.near`) is on the SDK's own hardcoded deprecated-asset-id
list and gets filtered out client-side; the only Bitcoin-chain token that survives is
`symbol: "BTC(OMNI)"`. Search/pick `BTC(OMNI)`, not `BTC`.
- **Validation-blocking button states can render with no `id` at all.** `TradeFormButtons` only gives
`#do-trade-button` to the "no validation errors" case; a function-component validation state (e.g.
`RecipientNotSet`, `RecipientNotConfirmed` in `tradeButtonsMap.tsx`) renders its own
`TradeFormBlankButton` with no `id` prop. Match those by role/text
(`page.getByRole('button', { name: /.../i })`), not by a `#do-trade-button`/`swapButton` locator.
- **A controlled confirmation checkbox can lose a click under load.** `recipientConfirmationCheckbox`
(`#receiver-confirmation`) is driven by recipient-validation state that can still be settling right
after typing an address; a still-in-flight debounce can reset `confirmed` back to `false` immediately
after Playwright's `.check()` lands, surfacing as "Clicking the checkbox did not change its state" —
reproduces reliably only under concurrent test load (multiple workers), not in isolation. Retry via
`expect.poll(async () => { await checkbox.check(); return checkbox.isChecked() }).toBe(true)` instead
of a single `.check()`.
- The app's HashRouter makes `page.goto()` to a new `#/...` route a same-document navigation —
`bridgingSdk`'s available-provider set is a page-lifetime singleton seeded once at module load, so a
test that switches providers mid-test (`mocks.launchDarkly.setFlag` again) needs an actual
`page.reload()` after the new hash is already in the address bar for the switch to take effect.
- The Bungee quote fixture's `output.amount` is a single captured absolute number, unrelated to whatever
amount a given test's sell leg actually produces — `mocks/bungee.ts`'s `/quote` handler scales every
amount field (and their USD counterparts) proportionally to the live `inputAmount` query param to keep
the fixture's own input:output ratio (and therefore price impact) realistic for any sell amount.

## Known issues (discovered this session, unresolved)

- **`mocks.balances.set()` called after the app already has an open SSE connection (e.g. from inside a
Expand Down
10 changes: 10 additions & 0 deletions apps/cowswap-e2e-tests/src/fixtures/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { createRpcProxyHandle, type RpcProxyHandle } from './rpcProxy'

import { installAllowances, type AllowancesMock } from '../mocks/allowances'
import { installBalances, type BalancesMock } from '../mocks/balances'
import { installBungee, type BungeeMock } from '../mocks/bungee'
import { installCowProtocolApi, type CowProtocolApiMock } from '../mocks/cowProtocolApi'
import { installEthBlockNumber } from '../mocks/ethBlockNumber'
import { installEthEstimateGas } from '../mocks/ethEstimateGas'
import { installEthGetCode, type EthGetCodeMock } from '../mocks/ethGetCode'
import { installEthGetTransactionCount } from '../mocks/ethGetTransactionCount'
import { installLaunchDarkly, type LaunchDarklyMock } from '../mocks/launchDarkly'
import { installMulticall3 } from '../mocks/multicall3'
import { installNearIntents, type NearIntentsMock } from '../mocks/nearIntents'
import { installSafeSdk, type SafeSdkMock } from '../mocks/safeSdk'
import { installTokenLists, type TokenListsMock } from '../mocks/tokenLists'
import { installUsdPrices, type UsdPricesMock } from '../mocks/usdPrices'
Expand Down Expand Up @@ -44,6 +46,8 @@ export interface SharedFixtures {
ethGetCode: EthGetCodeMock
tokenLists: TokenListsMock
safeSdk: SafeSdkMock
bungee: BungeeMock
nearIntents: NearIntentsMock
launchDarkly: LaunchDarklyMock
usdPrices: UsdPricesMock
}
Expand Down Expand Up @@ -123,6 +127,8 @@ export const sharedFixtures: Fixtures<
installMulticall3(context, { allowances })
const tokenLists = installTokenLists(context)
const safeSdk = installSafeSdk(context)
const bungee = installBungee(context)
const nearIntents = installNearIntents(context)
const launchDarkly = installLaunchDarkly(context)
const usdPrices = installUsdPrices(context)

Expand All @@ -133,12 +139,16 @@ export const sharedFixtures: Fixtures<
ethGetCode,
tokenLists,
safeSdk,
bungee,
nearIntents,
launchDarkly,
usdPrices,
})

ethGetCode.reset()
tokenLists.reset()
bungee.reset()
nearIntents.reset()
await launchDarkly.reset()
usdPrices.reset()
await safeSdk.disable()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"success": true,
"statusCode": 200,
"result": [
{
"chainId": 8453,
"address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"name": "USDC",
"symbol": "USDC",
"decimals": 6,
"logoURI": "https://assets.coingecko.com/coins/images/6319/large/USDC.png?1769615602",
"icon": "https://assets.coingecko.com/coins/images/6319/large/USDC.png?1769615602"
},
{
"chainId": 8453,
"address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"name": "Ether",
"symbol": "ETH",
"decimals": 18,
"logoURI": "https://media.socket.tech/networks/ethereum.svg",
"icon": "https://media.socket.tech/networks/ethereum.svg"
},
{
"chainId": 8453,
"address": "0x4e107a0000db66f0e9fd2039288bf811dd1f9c74",
"name": "Velora",
"symbol": "VLR",
"decimals": 18,
"logoURI": "https://assets.coingecko.com/coins/images/55593/large/PNG_Round.png?1746809175",
"icon": "https://assets.coingecko.com/coins/images/55593/large/PNG_Round.png?1746809175"
},
{
"chainId": 8453,
"address": "0xd652c5425aea2afd5fb142e120fecf79e18fafc3",
"name": "PoolTogether",
"symbol": "POOL",
"decimals": 18,
"logoURI": "https://assets.coingecko.com/coins/images/14003/large/PoolTogether.png?1696513732",
"icon": "https://assets.coingecko.com/coins/images/14003/large/PoolTogether.png?1696513732"
},
{
"chainId": 8453,
"address": "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2",
"name": "L2 Standard Bridged USDT Base ",
"symbol": "USDT",
"decimals": 6,
"logoURI": "https://assets.coingecko.com/coins/images/39963/large/usdt.png?1724952731",
"icon": "https://assets.coingecko.com/coins/images/39963/large/usdt.png?1724952731"
},
{
"chainId": 8453,
"address": "0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca",
"name": "Bridged USDC Base ",
"symbol": "USDBC",
"decimals": 6,
"logoURI": "https://assets.coingecko.com/coins/images/31164/large/baseusdc.jpg?1696529993",
"icon": "https://assets.coingecko.com/coins/images/31164/large/baseusdc.jpg?1696529993"
},
{
"chainId": 8453,
"address": "0x50c5725949a6f0c72e6c4a641f24049a917db0cb",
"name": "L2 Standard Bridged DAI Base ",
"symbol": "DAI",
"decimals": 18,
"logoURI": "https://assets.coingecko.com/coins/images/39807/large/dai.png?1724126571",
"icon": "https://assets.coingecko.com/coins/images/39807/large/dai.png?1724126571"
},
{
"chainId": 8453,
"address": "0x4158734d47fc9692176b5085e0f52ee0da5d47f1",
"name": "Balancer",
"symbol": "BAL",
"decimals": 18,
"logoURI": "https://assets.coingecko.com/coins/images/11683/large/Balancer.png?1696511572",
"icon": "https://assets.coingecko.com/coins/images/11683/large/Balancer.png?1696511572"
}
],
"message": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"success": true,
"statusCode": 200,
"result": [
{
"chainId": 1,
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"name": "USDC",
"symbol": "USDC",
"decimals": 6,
"logoURI": "https://assets.coingecko.com/coins/images/6319/large/USDC.png?1769615602",
"icon": "https://assets.coingecko.com/coins/images/6319/large/USDC.png?1769615602"
},
{
"chainId": 1,
"address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"name": "Tether",
"symbol": "USDT",
"decimals": 6,
"logoURI": "https://assets.coingecko.com/coins/images/325/large/Tether.png?1696501661",
"icon": "https://assets.coingecko.com/coins/images/325/large/Tether.png?1696501661"
}
],
"message": null
}
Loading
Loading