diff --git a/extension/e2e-tests/blockaidScan.errors.test.ts b/extension/e2e-tests/blockaidScan.errors.test.ts index a9e140d37d..df509a2af3 100644 --- a/extension/e2e-tests/blockaidScan.errors.test.ts +++ b/extension/e2e-tests/blockaidScan.errors.test.ts @@ -83,9 +83,8 @@ test.describe("BlockAid Scan - Edge Cases", () => { }); await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); @@ -123,9 +122,8 @@ test.describe("BlockAid Scan - Edge Cases", () => { }); await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); @@ -239,9 +237,8 @@ test.describe("BlockAid Scan - Edge Cases", () => { }); await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); diff --git a/extension/e2e-tests/blockaidScan.malicious.test.ts b/extension/e2e-tests/blockaidScan.malicious.test.ts index 3d1f8f6e37..f9bed8ddda 100644 --- a/extension/e2e-tests/blockaidScan.malicious.test.ts +++ b/extension/e2e-tests/blockaidScan.malicious.test.ts @@ -121,9 +121,8 @@ test.describe("BlockAid Scan - Malicious States", () => { }); await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); @@ -344,9 +343,8 @@ test.describe("BlockAid Scan - Malicious States", () => { // Go to send payment to an M-address (requires memo) await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GA6SXIZIKLJHCZI2KEOBEUUOFMM4JUPPM2UTWX6STAWT25JWIEUFIMFF"); diff --git a/extension/e2e-tests/blockaidScan.safe.test.ts b/extension/e2e-tests/blockaidScan.safe.test.ts index 6af71fd218..145a99cb2e 100644 --- a/extension/e2e-tests/blockaidScan.safe.test.ts +++ b/extension/e2e-tests/blockaidScan.safe.test.ts @@ -126,9 +126,8 @@ test.describe("BlockAid Scan - Safe States (No Override)", () => { }); await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); diff --git a/extension/e2e-tests/blockaidScan.suspicious.test.ts b/extension/e2e-tests/blockaidScan.suspicious.test.ts index 8dc54b443a..2663f090f1 100644 --- a/extension/e2e-tests/blockaidScan.suspicious.test.ts +++ b/extension/e2e-tests/blockaidScan.suspicious.test.ts @@ -109,9 +109,8 @@ test.describe("BlockAid Scan - Suspicious States", () => { }); await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); @@ -323,9 +322,8 @@ test.describe("BlockAid Scan - Suspicious States", () => { // Go to send payment to an M-address (requires memo) await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GA6SXIZIKLJHCZI2KEOBEUUOFMM4JUPPM2UTWX6STAWT25JWIEUFIMFF"); diff --git a/extension/e2e-tests/blockaidScan.unable.test.ts b/extension/e2e-tests/blockaidScan.unable.test.ts index b7432651f2..92d0c1c55e 100644 --- a/extension/e2e-tests/blockaidScan.unable.test.ts +++ b/extension/e2e-tests/blockaidScan.unable.test.ts @@ -129,9 +129,8 @@ test.describe("BlockAid Scan - Unable to Scan States", () => { // Navigate to send payment await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); // Use a valid test address await page .getByTestId("send-to-input") @@ -368,9 +367,8 @@ test.describe("BlockAid Scan - Unable to Scan States", () => { // Go to send payment to an M-address (requires memo) await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GA6SXIZIKLJHCZI2KEOBEUUOFMM4JUPPM2UTWX6STAWT25JWIEUFIMFF"); diff --git a/extension/e2e-tests/integration-tests/sendIntegration.test.ts b/extension/e2e-tests/integration-tests/sendIntegration.test.ts index 66d1e3cdd5..7508ad38cc 100644 --- a/extension/e2e-tests/integration-tests/sendIntegration.test.ts +++ b/extension/e2e-tests/integration-tests/sendIntegration.test.ts @@ -46,9 +46,8 @@ test("Send persists inputs and submits to network", async ({ }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") @@ -137,12 +136,11 @@ test("Send XLM payments to recent federated addresses", async ({ await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill("freighter.pb*lobstr.co"); - await expect(page.getByTestId("send-to-identicon")).toBeVisible(); + await expect(page.getByTestId("send-to-suggestion-button")).toBeVisible(); await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); @@ -168,13 +166,23 @@ test("Send XLM payments to recent federated addresses", async ({ await page.getByText("Done").click(); await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await expect(page.getByText("Send to")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); - await page.getByTestId("address-tile").click(); + // Recents should be visible on the send-to screen before typing anything. + await expect(page.getByText("Recents")).toBeVisible(); + // Recents should remain visible while the user is typing. + await page.getByTestId("send-to-input").fill("freighter.pb*lobstr.co"); + await expect(page.getByText("Recents")).toBeVisible(); + + // Recents should remain visible once a suggestion resolves. + await expect(page.getByTestId("send-to-suggestion-button")).toBeVisible(); await expect(page.getByText("Recents")).toBeVisible(); + // Clear the input and use the recent address directly. + await page.getByTestId("send-to-input").fill(""); + await page.getByTestId("recent-address-button").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); @@ -215,9 +223,8 @@ test("Send XLM payment to C address", async ({ await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(TEST_TOKEN_ADDRESS); await page.getByText("Continue").click({ force: true }); @@ -259,9 +266,8 @@ test("Send XLM payment to M address", async ({ await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(TEST_M_ADDRESS); await page.getByText("Continue").click(); @@ -403,22 +409,29 @@ test("Send token payment to C address", async ({ } await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(TEST_TOKEN_ADDRESS); await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.locator(".SendAmount__EditDestAsset").click(); + await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByText("E2E").click(); + await page + .locator(".Send__step:not(.Send__step--hidden)") + .getByText("E2E") + .click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await page.getByTestId("send-amount-amount-input").fill(".001"); - await page.getByText("Review Send").click({ force: true }); + + // Wait for auto-simulation to complete before clicking Review Send. + // handleContinue returns early if simulationState.state === LOADING. + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 30000 }); + await reviewSendButton.click(); await expect(page.getByText("You are sending")).toBeVisible({ timeout: 60000, diff --git a/extension/e2e-tests/loadAccount.test.ts b/extension/e2e-tests/loadAccount.test.ts index 995d3173b1..01b89fc309 100644 --- a/extension/e2e-tests/loadAccount.test.ts +++ b/extension/e2e-tests/loadAccount.test.ts @@ -642,6 +642,7 @@ test("Loads collectibles data with successful metadata", async ({ extensionId, context, }) => { + test.slow(); const stubOverrides = async () => { await stubCollectibles(page, true); }; @@ -760,6 +761,12 @@ test("Loads collectibles data with successful metadata", async ({ // test that the send button navigates to the send payment page await page.getByTestId("CollectibleDetail__footer__buttons__send").click(); + // Send flow starts at the destination step when launched from a collectible. + await expect(page.getByTestId("send-to-input")).toBeVisible(); + await page + .getByTestId("send-to-input") + .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); + await page.getByText("Continue").click(); await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); await expect( page.getByTestId("SelectedCollectible__base-info__row__name__value"), diff --git a/extension/e2e-tests/memo.test.ts b/extension/e2e-tests/memo.test.ts index 1a3643826b..acef64b123 100644 --- a/extension/e2e-tests/memo.test.ts +++ b/extension/e2e-tests/memo.test.ts @@ -31,9 +31,8 @@ test("Send payment shows memo required warning when destination requires memo", await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); await page.getByText("Continue").click(); @@ -69,9 +68,8 @@ test("Send payment allows submission after adding memo to memo-required address" await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); await page.getByText("Continue").click(); @@ -135,9 +133,8 @@ test("Send payment returns to review modal after adding memo from review flow", await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); await page.getByText("Continue").click(); @@ -190,9 +187,8 @@ test("Send payment returns to review modal after cancelling memo editor from rev await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); await page.getByText("Continue").click(); @@ -243,9 +239,8 @@ test("Send payment shows memo value directly when memo is added before review", await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); await page.getByText("Continue").click(); @@ -321,10 +316,10 @@ test("Send payment shows Add Memo when switching from non-memo-required to memo- await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); // First, set a non-memo-required address - await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").fill(NON_MEMO_REQUIRED_ADDRESS); await page.getByText("Continue").click(); @@ -356,7 +351,9 @@ test("Send payment shows Add Memo when switching from non-memo-required to memo- await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").clear(); await page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); - await page.getByText("Continue").click(); + // Click the suggestion button rather than Continue — it only appears after the + // debounce settles and the new fetch resolves, confirming the address is saved. + await page.getByTestId("send-to-suggestion-button").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); // Make sure amount is still 1 XLM after switching addresses @@ -388,9 +385,8 @@ test("Send payment shows Add Memo after cancelling review and returning to memo- await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); await page.getByText("Continue").click(); @@ -463,11 +459,11 @@ test("Send classic token to G address allows memo", async ({ await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); const G_ADDRESS = "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"; - await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").fill(G_ADDRESS); await page.getByText("Continue").click(); @@ -504,9 +500,9 @@ test("Send classic token to M address doesn't allow memo", async ({ await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); - await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").fill(TEST_M_ADDRESS); await page.getByText("Continue").click(); @@ -536,13 +532,11 @@ test("Send custom token without Soroban mux support to G address disables memo", await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible({ - timeout: 30000, - }); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); const G_ADDRESS = "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"; - await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").fill(G_ADDRESS); await page.getByText("Continue").click(); @@ -551,8 +545,9 @@ test("Send custom token without Soroban mux support to G address disables memo", }); // Select custom token - await page.locator(".SendAmount__EditDestAsset").click(); + await page.getByTestId("send-amount-edit-dest-asset").click(); await page + .locator(".Send__step:not(.Send__step--hidden)") .getByTestId(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) .click({ force: true }); @@ -626,17 +621,18 @@ test("Send custom token without Soroban mux support to M address is disabled", a await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); - await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").fill(TEST_M_ADDRESS); await page.getByText("Continue").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); // Select custom token - await page.locator(".SendAmount__EditDestAsset").click(); + await page.getByTestId("send-amount-edit-dest-asset").click(); await page + .locator(".Send__step:not(.Send__step--hidden)") .getByTestId(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) .click({ force: true }); @@ -674,19 +670,20 @@ test("Send custom token with Soroban mux support to G address allows memo", asyn await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); const G_ADDRESS = "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"; - await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").fill(G_ADDRESS); await page.getByText("Continue").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); // Select custom token - await page.locator(".SendAmount__EditDestAsset").click(); + await page.getByTestId("send-amount-edit-dest-asset").click(); await page + .locator(".Send__step:not(.Send__step--hidden)") .getByTestId(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) .click({ force: true }); @@ -740,17 +737,18 @@ test("Send custom token with Soroban mux support to M address disables memo", as await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); - await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").fill(TEST_M_ADDRESS); await page.getByText("Continue").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); // Select custom token - await page.locator(".SendAmount__EditDestAsset").click(); + await page.getByTestId("send-amount-edit-dest-asset").click(); await page + .locator(".Send__step:not(.Send__step--hidden)") .getByTestId(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) .click({ force: true }); diff --git a/extension/e2e-tests/reviewTxFees.test.ts b/extension/e2e-tests/reviewTxFees.test.ts index af288b8e79..d8669dff39 100644 --- a/extension/e2e-tests/reviewTxFees.test.ts +++ b/extension/e2e-tests/reviewTxFees.test.ts @@ -33,13 +33,11 @@ test("Fee breakdown pane shows Soroban fees for token send", async ({ // Navigate to token send via Asset Detail await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("0.1"); - - // Set destination (address tile in SendAmount opens the SendTo step) - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.1"); // Wait for simulation to finish, then click Review Send const reviewSendButton = page.getByTestId("send-amount-btn-continue"); @@ -89,10 +87,8 @@ test("Fee breakdown pane shows only total fee for classic XLM send", async ({ await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - // Navigate to Send To screen - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await expect(page.getByTestId("send-to-input")).toBeVisible(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click({ force: true }); @@ -169,25 +165,16 @@ test("Collectible simulation falls back to BASE_FEE on first mount when Redux tr await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - // Confirm Redux transactionFee is still the empty-string default - await expect(page.getByTestId("send-amount-fee-display")).toHaveText( - "0.00001 XLM", - ); + await expect(page.getByTestId("token-list")).toBeVisible(); - // Select a collectible — Redux transactionFee remains "" - await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByTestId("account-tab-collectibles").click(); await page.getByText("Stellar Frog 1").click(); - await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); // Set destination — triggers first-mount simulation with transactionFee="" - await page.getByTestId("address-tile").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); // Simulation must complete: Continue button becomes enabled and fee shows // baseFee(0.00001) + resourceFee(0.00001) = 0.00002 XLM @@ -222,18 +209,14 @@ test("Fee breakdown pane shows Soroban fees for collectible send", async ({ await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); - // Select a collectible - await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByTestId("account-tab-collectibles").click(); await page.getByText("Stellar Frog 1").click(); - await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); // Set destination - await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); // Wait for simulation to finish, then click Review Send const reviewSendButton = page.getByTestId("send-collectible-btn-continue"); @@ -290,35 +273,41 @@ test("Custom token without destination — full fee lifecycle in EditSettings an await loginToTestAccount({ page, extensionId, context, stubOverrides }); - // Navigate to token send (no destination set yet) + // Navigate to token send and set destination. + // Auto-simulation fires as soon as destination is set (isToken=true), so the + // fee display will update to the simulated total without needing an amount. await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - // Without a destination, no auto-simulation fires — fee display stays at base fee + // Simulation fires on destination set — wait for the simulated total await expect(page.getByTestId("send-amount-fee-display")).toHaveText( - "0.00001 XLM", + "0.0093338 XLM", + { timeout: 10000 }, ); // ── Open Edit Settings ────────────────────────────────────────────────────── await page.getByTestId("send-amount-btn-fee").click(); await expect(page.getByText("Inclusion Fee")).toBeVisible(); - // No auto-simulation yet — shows recommended base fee + // EditSettings always shows the inclusion (base) fee, not the total await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( "0.00001", ); - // ── Open FeesPane — no destination so simulation does NOT fire ─────────── - // FeesPane shows the base fee as inclusion/total and "None" for resource, - // matching mobile behaviour (no error, no simulated amounts). + // ── Open FeesPane — simulation has completed ───────────────────────────── await page.getByTestId("edit-settings-fees-info-btn").click(); await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); await expect(page.getByTestId("review-tx-inclusion-fee")).toHaveText( "0.00001 XLM", ); - await expect(page.getByTestId("review-tx-resource-fee")).toHaveText("None"); + await expect(page.getByTestId("review-tx-resource-fee")).toHaveText( + "0.0093238 XLM", + ); await expect(page.getByTestId("review-tx-total-fee")).toHaveText( - "0.00001 XLM", + "0.0093338 XLM", ); await expect(page.getByTestId("review-tx-fees-description")).toContainText( "Soroban", @@ -335,14 +324,16 @@ test("Custom token without destination — full fee lifecycle in EditSettings an "0.00005", ); - // ── Open FeesPane again — draft fee reflected, resource still "None" ────── + // ── Open FeesPane again — draft not saved yet, total reflects saved simulation ── await page.getByTestId("edit-settings-fees-info-btn").click(); await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); - // No simulation data: total = draft inclusion fee only + // Draft 0.00005 is unsaved; FeesPane shows the last simulated total await expect(page.getByTestId("review-tx-total-fee")).toHaveText( - "0.00005 XLM", + "0.0093338 XLM", + ); + await expect(page.getByTestId("review-tx-resource-fee")).toHaveText( + "0.0093238 XLM", ); - await expect(page.getByTestId("review-tx-resource-fee")).toHaveText("None"); // ── Close FeesPane → draft still in the input ───────────────────────────── await page.getByTestId("review-tx-fees-close-btn").click(); @@ -354,9 +345,10 @@ test("Custom token without destination — full fee lifecycle in EditSettings an // ── Save the custom fee ─────────────────────────────────────────────────── await page.getByRole("button", { name: "Save" }).click(); await expect(page.getByText("Inclusion Fee")).not.toBeVisible(); - // No destination → no simulation → fee display shows the saved inclusion fee + // Re-simulation runs with baseFee=0.00005 → total = 0.00005 + 0.0093238 = 0.0093738 await expect(page.getByTestId("send-amount-fee-display")).toHaveText( - "0.00005 XLM", + "0.0093738 XLM", + { timeout: 10000 }, ); // ── Reopen Edit Settings — must show saved fee, not the base default ─────── @@ -366,16 +358,17 @@ test("Custom token without destination — full fee lifecycle in EditSettings an "0.00005", ); - // ── Open FeesPane from re-opened settings — saved fee reflected ──────────── - // Still no destination, so resource stays "None" and total = inclusion fee only. + // ── Open FeesPane from re-opened settings — saved fee and re-simulation reflected ── await page.getByTestId("edit-settings-fees-info-btn").click(); await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); await expect(page.getByTestId("review-tx-inclusion-fee")).toHaveText( "0.00005 XLM", ); - await expect(page.getByTestId("review-tx-resource-fee")).toHaveText("None"); + await expect(page.getByTestId("review-tx-resource-fee")).toHaveText( + "0.0093238 XLM", + ); await expect(page.getByTestId("review-tx-total-fee")).toHaveText( - "0.00005 XLM", + "0.0093738 XLM", ); await expect(page.getByTestId("review-tx-fees-description")).toContainText( "Soroban", @@ -403,13 +396,13 @@ test("Custom token with recipient — full fee lifecycle in EditSettings and Fee // Navigate to token send and set destination await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("0.1"); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.1"); + // Wait for auto-simulation: total = baseFee(0.00001) + resource(0.0093238) await expect(page.getByTestId("send-amount-fee-display")).toHaveText( "0.0093338 XLM", @@ -523,13 +516,13 @@ test("Custom fee resets to default when re-entering send flow from home screen", // ── First session: set custom fee ───────────────────────────────────────── await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("0.1"); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.1"); + // Wait for simulation await expect(page.getByTestId("send-amount-fee-display")).toHaveText( "0.0093338 XLM", @@ -550,7 +543,12 @@ test("Custom fee resets to default when re-entering send flow from home screen", ); // ── Navigate back to home screen ────────────────────────────────────────── - await page.getByTestId("BackButton").click(); + // The linearized flow renders all visited steps in the DOM simultaneously; + // scope to the active step to avoid strict-mode violation. + await page + .locator(".Send__step:not(.Send__step--hidden)") + .getByTestId("BackButton") + .click(); // goBack() dispatches resetSubmission() (clears destination / fees / state) // and navigates to ROUTES.account (the main AccountView) await expect(page.getByTestId("account-view")).toBeVisible({ @@ -558,14 +556,23 @@ test("Custom fee resets to default when re-entering send flow from home screen", }); // ── Second session: re-enter the same send flow ─────────────────────────── - await page.getByText("E2E").click(); + // closeSendFlow returns to account view with the asset detail still open + // (returnTo="asset_detail"), so asset-detail-send-button is directly available. + await expect(page.getByTestId("asset-detail-send-button")).toBeVisible({ + timeout: 5000, + }); await page.getByTestId("asset-detail-send-button").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - // resetSubmission cleared destination → no auto-simulation fires on mount. - // Fee display shows the base fee, NOT the previous override total (0.0093738). + // resetSubmission cleared fee state. Auto-simulation fires on destination set, + // using the reset base fee (0.00001) — NOT the previous override (0.00005). + // Total = 0.00001 + 0.0093238 = 0.0093338 (not the previous 0.0093738). await expect(page.getByTestId("send-amount-fee-display")).toHaveText( - "0.00001 XLM", + "0.0093338 XLM", + { timeout: 10000 }, ); // ── EditSettings must show the default inclusion fee, not the saved "0.00005" ─ @@ -575,11 +582,11 @@ test("Custom fee resets to default when re-entering send flow from home screen", "0.00001", ); - // ── FeesPane shows base fee — no destination means no simulation ───────── + // ── FeesPane shows simulated total using the reset base fee ───────────── await page.getByTestId("edit-settings-fees-info-btn").click(); await expect(page.getByTestId("review-tx-fees-pane")).toBeVisible(); await expect(page.getByTestId("review-tx-total-fee")).toHaveText( - "0.00001 XLM", + "0.0093338 XLM", ); }); @@ -603,13 +610,11 @@ test("Auto-simulation updates fee display on SendAmount before Review Send", asy // Navigate to token send via Asset Detail await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("0.1"); - - // Set destination — auto-simulation fires automatically after returning to SendAmount - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.1"); // Without clicking "Review Send", the fee display should update to the // simulated total (inclusion + resource). @@ -634,10 +639,13 @@ test("Soroban token — manually set fee is preserved when recipient is selected await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await page.getByTestId("send-amount-amount-input").fill("0.1"); - // Set custom fee before picking a recipient + // Set custom fee before changing recipient await page.getByTestId("send-amount-btn-fee").click(); await expect(page.getByText("Inclusion Fee")).toBeVisible(); await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( @@ -647,9 +655,9 @@ test("Soroban token — manually set fee is preserved when recipient is selected await page.getByRole("button", { name: "Save" }).click(); await expect(page.getByText("Inclusion Fee")).not.toBeVisible(); - // Select recipient — SendAmount remounts; fee must survive via Redux persistence + // Change recipient — SendAmount remounts; fee must survive via Redux persistence await page.getByTestId("address-tile").click(); - await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION_2); await page.getByText("Continue").click(); // Re-simulation uses saved baseFee=0.00005 → total = 0.00005 + 0.0093238 @@ -675,6 +683,10 @@ test("Classic send — manually set fee is applied and shown in settings", async await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await expect(page.getByTestId("send-amount-fee-display")).toHaveText( "0.00001 XLM", @@ -710,10 +722,8 @@ test("Classic send — manually set fee carries through to Review Send", async ( await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); - await expect(page.getByTestId("send-to-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click({ force: true }); @@ -758,6 +768,10 @@ test("Classic send — manually set fee resets when re-entering send flow from h await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await page.getByTestId("send-amount-btn-fee").click(); @@ -769,12 +783,22 @@ test("Classic send — manually set fee resets when re-entering send flow from h "0.00005 XLM", ); - await page.getByTestId("BackButton").click(); + // The linearized Send flow renders all visited steps in the DOM simultaneously + // (hiding non-active ones). After visiting SELECT_SOURCE_ASSET → DESTINATION → + // AMOUNT there are 3 BackButton elements; scope to the active step only. + await page + .locator(".Send__step:not(.Send__step--hidden)") + .getByTestId("BackButton") + .click(); await expect(page.getByTestId("account-view")).toBeVisible({ timeout: 10000, }); await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await expect(page.getByTestId("send-amount-fee-display")).toHaveText( "0.00001 XLM", @@ -797,9 +821,13 @@ test("Classic send — manually set fee is preserved across change of recipient" await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - // Set custom fee before picking a recipient + // Set custom fee before changing recipient await page.getByTestId("send-amount-btn-fee").click(); await expect(page.getByText("Transaction Fee")).toBeVisible(); await page.getByTestId("edit-tx-settings-fee-input").fill("0.00005"); @@ -809,10 +837,10 @@ test("Classic send — manually set fee is preserved across change of recipient" "0.00005 XLM", ); - // Select recipient — SendAmount remounts; no simulation runs for classic + // Change recipient — no simulation runs for classic await page.getByTestId("address-tile").click(); await expect(page.getByTestId("send-to-input")).toBeVisible(); - await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION_2); await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("send-amount-fee-display")).toHaveText( @@ -853,13 +881,11 @@ test("Re-simulation on destination change shows correct inclusion fee in EditSet // Navigate to token send await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("0.1"); - - // Set first destination — auto-simulation fires - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.1"); // Wait for first simulation to finish await expect(page.getByTestId("send-amount-fee-display")).toHaveText( @@ -923,12 +949,15 @@ test("FeesPane shows inclusion/resource rows immediately for Soroban — resourc await loginToTestAccount({ page, extensionId, context, stubOverrides }); - // Navigate to token send (no destination set) + // Navigate to token send and set destination (no amount — no auto-simulation fires) await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - // Open EditSettings — this triggers simulation (which is blocked) + // Open EditSettings — no amount set so simulation has not fired (stub blocks if it does) await page.getByTestId("send-amount-btn-fee").click(); await expect(page.getByText("Inclusion Fee")).toBeVisible(); @@ -978,15 +1007,14 @@ test("FeesPane shows — for all fee rows when simulation fails", async ({ await loginToTestAccount({ page, extensionId, context, stubOverrides }); - // Navigate to token send and set a destination to trigger auto-simulation + // Navigate to token send and set destination + amount to trigger auto-simulation await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("0.1"); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.1"); // Wait for the simulation to fail — the Continue button becomes disabled const continueButton = page.getByTestId("send-amount-btn-continue"); @@ -1025,12 +1053,11 @@ test("Send settings Default button resets to recommended fee after saving custom // Navigate to token send and trigger Soroban simulation await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("0.1"); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.1"); // Wait for initial simulated total fee display await expect(page.getByTestId("send-amount-fee-display")).toHaveText( @@ -1071,10 +1098,11 @@ const COLLECTIBLE_CONTRACT = // Assumes stubSimulateSendCollectible and stubContractSpec are already set up. async function navigateToCollectibleSend(page: Page) { await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByTestId("account-tab-collectibles").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByText("Stellar Frog 1").click(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); } @@ -1096,8 +1124,9 @@ test("Collectible — manually set fee before destination is used in simulation" await loginToTestAccount({ page, extensionId, context, stubOverrides }); await navigateToCollectibleSend(page); + // SelectedCollectible is visible; simulation fires as destination is already set - // Set a custom fee before providing a destination + // Set a custom fee (destination already set — will trigger re-simulation with custom fee) await page.getByTestId("send-amount-btn-fee").click(); await expect(page.getByText("Inclusion Fee")).toBeVisible(); await expect(page.getByTestId("edit-tx-settings-fee-input")).toHaveValue( @@ -1106,16 +1135,8 @@ test("Collectible — manually set fee before destination is used in simulation" await page.getByTestId("edit-tx-settings-fee-input").fill("0.00005"); await page.getByRole("button", { name: "Save" }).click(); await expect(page.getByText("Inclusion Fee")).not.toBeVisible(); - await expect(page.getByTestId("send-amount-fee-display")).toHaveText( - "0.00005 XLM", - ); - - // Set destination — simulation fires with saved custom fee - await page.getByTestId("address-tile").click(); - await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); - await page.getByText("Continue").click({ force: true }); - // Re-simulation uses baseFee=0.00005 → total = 0.00005 + 0.00001 = 0.00006 + // Simulation runs with baseFee=0.00005 → total = 0.00005 + 0.00001 = 0.00006 const continueButton = page.getByTestId("send-collectible-btn-continue"); await expect(continueButton).toBeEnabled({ timeout: 10000 }); await expect(page.getByTestId("send-amount-fee-display")).toHaveText( @@ -1145,11 +1166,7 @@ test("Collectible — manually set fee after simulation triggers re-simulation", await loginToTestAccount({ page, extensionId, context, stubOverrides }); await navigateToCollectibleSend(page); - - // Set destination — auto-simulation fires with BASE_FEE - await page.getByTestId("address-tile").click(); - await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); - await page.getByText("Continue").click({ force: true }); + // Destination already set — auto-simulation fires with BASE_FEE const continueButton = page.getByTestId("send-collectible-btn-continue"); await expect(continueButton).toBeEnabled({ timeout: 10000 }); @@ -1197,11 +1214,7 @@ test("Collectible — Default button resets to recommended fee after saving cust await loginToTestAccount({ page, extensionId, context, stubOverrides }); await navigateToCollectibleSend(page); - - // Set destination and wait for simulation - await page.getByTestId("address-tile").click(); - await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); - await page.getByText("Continue").click({ force: true }); + // Destination already set — wait for simulation to complete const continueButton = page.getByTestId("send-collectible-btn-continue"); await expect(continueButton).toBeEnabled({ timeout: 10000 }); diff --git a/extension/e2e-tests/sendCollectible.test.ts b/extension/e2e-tests/sendCollectible.test.ts index ebbb74fe79..0b4fc8d2a0 100644 --- a/extension/e2e-tests/sendCollectible.test.ts +++ b/extension/e2e-tests/sendCollectible.test.ts @@ -13,6 +13,7 @@ test("Send collectible with metadata", async ({ extensionId, context, }) => { + test.slow(); const stubOverrides = async () => { await stubSimulateSendCollectible(page); }; @@ -27,14 +28,15 @@ test("Send collectible with metadata", async ({ await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await expect(page.getByTestId("send-amount-fee-display")).toHaveText( - "0.00001 XLM", - ); - await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByTestId("account-tab-collectibles").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByText("Stellar Frog 1").click(); + await page + .getByTestId("send-to-input") + .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); + await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); await expect( page.getByTestId("SelectedCollectible__base-info__row__name__value"), @@ -54,12 +56,6 @@ test("Send collectible with metadata", async ({ "https://nftcalendar.io/storage/uploads/events/2023/5/NeToOQbYtaJILHMnkigEAsA6ckKYe2GAA4ppAOSp.jpg", ); - await page.getByTestId("address-tile").click(); - await page - .getByTestId("send-to-input") - .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); - await page.getByText("Continue").click({ force: true }); - await page.getByText("Review Send").scrollIntoViewIfNeeded(); await page.getByText("Review Send").click({ force: true }); @@ -95,6 +91,7 @@ test("Send collectible without metadata", async ({ extensionId, context, }) => { + test.slow(); const stubOverrides = async () => { await stubCollectiblesUnsuccessfulMetadata(page); await stubSimulateSendCollectible(page); @@ -110,13 +107,17 @@ test("Send collectible without metadata", async ({ await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await expect(page.getByTestId("send-amount-fee-display")).toHaveText( - "0.00001 XLM", - ); - await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByTestId("account-tab-collectibles").click(); - await page.getByText("Stellar Frogs").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + + await page + .locator(".CollectiblesList__collection") + .filter({ hasText: "Stellar Frogs" }) + .click(); + + await page + .getByTestId("send-to-input") + .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); + await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); await expect( @@ -131,15 +132,11 @@ test("Send collectible without metadata", async ({ page.getByTestId("SelectedCollectible__base-info__row__tokenId__value"), ).toHaveText("3"); await expect( - page.getByTestId("account-collectible-placeholder"), + page + .getByTestId("SelectedCollectible") + .getByTestId("account-collectible-placeholder"), ).toBeVisible(); - await page.getByTestId("address-tile").click(); - await page - .getByTestId("send-to-input") - .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); - await page.getByText("Continue").click({ force: true }); - await page.getByText("Review Send").scrollIntoViewIfNeeded(); await page.getByText("Review Send").click(); @@ -176,6 +173,7 @@ test("Send collectible to M address when contract doesn't support muxed is disab extensionId, context, }) => { + test.slow(); const stubOverrides = async () => { await stubSimulateSendCollectible(page); }; @@ -190,19 +188,16 @@ test("Send collectible to M address when contract doesn't support muxed is disab await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); - await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByTestId("account-tab-collectibles").click(); await page.getByText("Stellar Frog 1").click(); - await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); - - // Try to send to M address (muxed address) - await page.getByTestId("address-tile").click(); + // Send to M address (muxed address) await page.getByTestId("send-to-input").fill(TEST_M_ADDRESS); await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); + // Wait for contract check to complete - warning banner should appear await expect( page.getByText( @@ -235,19 +230,16 @@ test("Send collectible with Soroban mux support to M address disables memo", asy await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); - await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByTestId("account-tab-collectibles").click(); await page.getByText("Stellar Frog 1").click(); - await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); - // Send to M address (muxed address) - await page.getByTestId("address-tile").click(); await page.getByTestId("send-to-input").fill(TEST_M_ADDRESS); await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); + // Verify NO warning banner is shown (contract supports muxed) await expect( page.getByText( @@ -288,6 +280,7 @@ test("Send collectible without Soroban mux support to G address disables memo", extensionId, context, }) => { + test.slow(); // Stub contract spec with muxed support = false await stubContractSpec( page, @@ -298,19 +291,18 @@ test("Send collectible without Soroban mux support to G address disables memo", await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByTestId("account-tab-collectibles").click(); - await page.getByText("Stellar Frog 1").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); - await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); + await page.getByText("Stellar Frog 1").click(); // Send to G address (regular address, not muxed) - await page.getByTestId("address-tile").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); + // Wait for the memo button const memoButton = page.getByTestId("send-amount-btn-memo"); await expect(memoButton).toBeEnabled({ timeout: 10000 }); @@ -344,6 +336,7 @@ test("Send collectible with Soroban mux support to G address allows memo", async extensionId, context, }) => { + test.slow(); const stubOverrides = async () => { await stubSimulateSendCollectible(page); }; @@ -357,21 +350,18 @@ test("Send collectible with Soroban mux support to G address allows memo", async await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await expect(page.getByTestId("token-list")).toBeVisible(); - await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByTestId("account-tab-collectibles").click(); await page.getByText("Stellar Frog 1").click(); - await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); - // Send to G address (regular address, not muxed) - await page.getByTestId("address-tile").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); + // Wait for the memo button const memoButton = page.getByTestId("send-amount-btn-memo"); await expect(memoButton).toBeEnabled({ timeout: 10000 }); diff --git a/extension/e2e-tests/sendPayment.test.ts b/extension/e2e-tests/sendPayment.test.ts index 91153aae97..fba9849543 100644 --- a/extension/e2e-tests/sendPayment.test.ts +++ b/extension/e2e-tests/sendPayment.test.ts @@ -1,5 +1,6 @@ import { test, expect } from "./test-fixtures"; -import { login, loginToTestAccount } from "./helpers/login"; +import { BrowserContext, Page } from "@playwright/test"; +import { login, loginToTestAccount, switchToMainnet } from "./helpers/login"; import { TEST_TOKEN_ADDRESS } from "./helpers/test-token"; import { stubAccountBalancesE2e, @@ -18,14 +19,66 @@ const UNFUNDED_DESTINATION = const FUNDED_DESTINATION = "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"; +function visibleTokenList(page: Page) { + return page.locator('[data-testid="token-list"]:visible').first(); +} + +async function stubSendTokenPrices(context: BrowserContext) { + await context.route("**/token-prices", async (route) => { + const request = route.request(); + let tokenIds = [] as string[]; + + if (request.method() === "POST") { + try { + const body = await request.postDataJSON(); + tokenIds = body.tokens || []; + } catch { + tokenIds = []; + } + } + + const data: Record< + string, + { currentPrice: string; percentagePriceChange24h: string } + > = { + native: { + currentPrice: "0.4079853099738737", + percentagePriceChange24h: "1.022345803068746424", + }, + "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5": { + currentPrice: "1.0000000000000000", + percentagePriceChange24h: "0.0000000000000000", + }, + }; + + for (const id of tokenIds) { + if (!data[id]) { + data[id] = { + currentPrice: "0.4079853099738737", + percentagePriceChange24h: "1.022345803068746424", + }; + } + } + + await route.fulfill({ json: { data } }); + }); +} + +async function clickVisibleBackButton(page: Page) { + await page.locator('[data-testid="BackButton"]:visible').first().click(); +} + test("Swap doesn't throw error when account is unfunded", async ({ page, extensionId, }) => { + test.slow(); await login({ page, extensionId }); await page.getByTestId("nav-link-swap").click(); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible({ + timeout: 15000, + }); }); test("Swap shows correct balances for assets", async ({ page, @@ -175,12 +228,12 @@ test("Swap shows correct balances for assets", async ({ await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-swap").click(); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible({ + timeout: 15000, + }); // Click on source asset tile to see asset list await page.getByTestId("swap-src-asset-tile").click(); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText( - "Swap from", - ); + await expect(page.getByText("Swap from")).toBeVisible(); await expect(page.getByText(/FOO/)).toBeVisible(); await expect(page.getByTestId("FOO-balance")).toContainText("100"); await expect(page.getByTestId("BAZ-balance")).toContainText("10"); @@ -196,10 +249,8 @@ test("Send doesn't throw error when account is unfunded", async ({ await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); - + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); @@ -230,14 +281,11 @@ test("Send XLM below minimum to unfunded destination shows warning", async ({ }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - // Select address to send to - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(UNFUNDED_DESTINATION); await page.getByText("Continue").click({ force: true }); - // Verify amount input is shown await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); // Enter amount less than 1 XLM @@ -283,14 +331,11 @@ test("Send XLM at minimum to unfunded destination proceeds without warning", asy }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - // Select address to send to - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(UNFUNDED_DESTINATION); await page.getByText("Continue").click({ force: true }); - // Verify amount input is shown await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); // Enter exactly 1 XLM (minimum required) @@ -341,18 +386,17 @@ test("Send non-native asset to unfunded destination shows destination missing wa }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - // Change asset to USDC - await page.getByTestId("send-amount-edit-dest-asset").click(); - await page.getByText("USDC").click(); - - // Select address to send to - await page.getByTestId("address-tile").click(); + // Select USDC directly from token picker + const usdcOption = page + .locator( + '[data-testid="SendRow-USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"], [data-testid="Select-assets-row-USDC"]', + ) + .first(); + await expect(usdcOption).toBeVisible({ timeout: 10000 }); + await usdcOption.click({ force: true }); await page.getByTestId("send-to-input").fill(UNFUNDED_DESTINATION); await page.getByText("Continue").click({ force: true }); - // Verify amount input is shown await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); // Enter USDC amount @@ -385,14 +429,11 @@ test("Send XLM to funded destination does not show unfunded warning", async ({ // Don't stub unfunded balances - the default stub will return isFunded: true await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - // Select address to send to (using funded destination) - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); await page.getByText("Continue").click({ force: true }); - // Verify amount input is shown await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); // Enter amount @@ -472,9 +513,8 @@ test("Send doesn't throw error when creating muxed account", async ({ }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(MUXED_ACCOUNT_ADDRESS); await expect( @@ -556,9 +596,8 @@ test("Send can review formatted inputs", async ({ }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page.getByTestId("send-to-input").fill(MUXED_ACCOUNT_ADDRESS); await expect( @@ -631,14 +670,14 @@ test.fixme("Send SAC to C address", async ({ page, extensionId, context }) => { }); await page.getByText("Done").click({ force: true }); - // send SAC to C address + // send SAC to C address — follow new linear flow: token picker → destination → amount await page.getByTestId("nav-link-send").click({ force: true }); + // Step 1: token picker — select USDC + await page.getByTestId("Select-assets-row-USDC").click({ force: true }); + // Step 2: destination await page.getByTestId("send-to-input").fill(TEST_TOKEN_ADDRESS); await page.getByText("Continue").click({ force: true }); - await page.getByTestId("send-amount-asset-select").click({ force: true }); - await page.getByTestId("Select-assets-row-USDC").click({ force: true }); - await expect(page.getByText("Send USDC")).toBeVisible(); await page.getByTestId("SendAmountSetMax").click({ force: true }); @@ -686,14 +725,20 @@ test("SendPayment persists amount and asset when navigating to choose address", await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("token-list")).toBeVisible(); + await page.getByTestId("SendRow-native").click(); + // Fill an address to proceed to AMOUNT + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await page.getByTestId("send-amount-amount-input").fill("100"); await expect(page.getByTestId("send-amount-amount-input")).toHaveValue("100"); + // Navigate to address picker from AMOUNT and back await page.getByTestId("address-tile").click(); await expect(page.getByTestId("send-to-input")).toBeVisible(); - await page.getByTestId("BackButton").click(); + await clickVisibleBackButton(page); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await expect(page.getByTestId("send-amount-amount-input")).toHaveValue("100"); @@ -715,14 +760,17 @@ test("SendPayment resets amount when user selects new asset", async ({ stubOverrides, }); - await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await goToTokenAmountStepFromHomeSend(page); await page.getByTestId("send-amount-amount-input").fill("50"); await expect(page.getByTestId("send-amount-amount-input")).toHaveValue("50"); - await page.locator(".SendAmount__EditDestAsset").click(); - await page.getByText("USDC").first().click({ force: true }); + // Change token via the token tile on the AMOUNT screen + await page.getByTestId("send-amount-edit-dest-asset").click(); + await visibleTokenList(page) + .getByText("USDC", { exact: true }) + .first() + .click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await expect(page.getByTestId("send-amount-amount-input")).toHaveValue("0"); @@ -745,28 +793,42 @@ test("SendPayment resets state when navigating back to account", async ({ }); await page.getByTestId("nav-link-send").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - await page.locator(".SendAmount__EditDestAsset").click(); - await page.getByText("USDC").first().click({ force: true }); - await page.getByTestId("send-amount-amount-input").fill("100"); - - await page.getByTestId("address-tile").click(); + await expect(visibleTokenList(page)).toBeVisible(); + await page.getByTestId("SendRow-native").click(); await page .getByTestId("send-to-input") .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); await page.getByText("Continue").click({ force: true }); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("BackButton").click(); + + // Change to USDC and enter amount + await page.getByTestId("send-amount-edit-dest-asset").click(); + await visibleTokenList(page) + .getByText("USDC", { exact: true }) + .first() + .click({ force: true }); + await page.getByTestId("send-amount-amount-input").fill("100"); + + // Press BackButton (X) to exit the flow + await clickVisibleBackButton(page); await expect(page.getByTestId("account-view")).toBeVisible(); + // Re-enter send flow: should start fresh at token picker await page.getByTestId("nav-link-send").click({ force: true }); + await expect(visibleTokenList(page)).toBeVisible(); + // Select XLM and navigate to AMOUNT to verify reset state + await page.getByTestId("SendRow-native").click(); + await page + .getByTestId("send-to-input") + .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); + await page.getByText("Continue").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await expect(page.getByTestId("send-amount-amount-input")).toHaveValue("0"); - // Verify XLM is selected (more reliable than checking for "0 XLM" text) - await expect(page.locator(".SendAmount__EditDestAsset")).toContainText("XLM"); + // Verify XLM is selected (not USDC from the previous session) + await expect(page.getByTestId("send-amount-edit-dest-asset")).toContainText( + "XLM", + ); }); test("Swap persists amount when navigating to choose source asset", async ({ @@ -778,7 +840,9 @@ test("Swap persists amount when navigating to choose source asset", async ({ await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-swap").click(); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible({ + timeout: 15000, + }); const amountInput = page.locator('input[type="text"]').first(); await amountInput.fill("100"); @@ -787,9 +851,11 @@ test("Swap persists amount when navigating to choose source asset", async ({ await page.getByTestId("swap-src-asset-tile").click({ force: true }); await expect(page.getByText("Swap from")).toBeVisible(); - await page.getByTestId("BackButton").click(); + await clickVisibleBackButton(page); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible({ + timeout: 15000, + }); await expect(amountInput).toHaveValue("100"); }); @@ -810,7 +876,9 @@ test("Swap resets amount when user selects new source asset", async ({ }); await page.getByTestId("nav-link-swap").click(); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible({ + timeout: 15000, + }); const amountInput = page.locator('input[type="text"]').first(); await amountInput.fill("50"); @@ -819,7 +887,9 @@ test("Swap resets amount when user selects new source asset", async ({ await page.getByTestId("swap-src-asset-tile").click({ force: true }); await page.getByText("USDC").first().click({ force: true }); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible({ + timeout: 15000, + }); await expect(amountInput).toHaveValue("0"); }); @@ -840,7 +910,9 @@ test("Swap preserves amount when selecting destination asset", async ({ }); await page.getByTestId("nav-link-swap").click(); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible({ + timeout: 15000, + }); const amountInput = page.locator('input[type="text"]').first(); await amountInput.fill("100"); @@ -849,7 +921,9 @@ test("Swap preserves amount when selecting destination asset", async ({ await page.getByTestId("swap-dst-asset-tile").click({ force: true }); await page.getByText("USDC").first().click({ force: true }); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible({ + timeout: 15000, + }); await expect(amountInput).toHaveValue("100"); }); @@ -870,7 +944,7 @@ test("Swap resets state when navigating back to account", async ({ }); await page.getByTestId("nav-link-swap").click(); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible(); const amountInput = page.locator('input[type="text"]').first(); await amountInput.fill("100"); @@ -881,13 +955,13 @@ test("Swap resets state when navigating back to account", async ({ await page.getByTestId("swap-dst-asset-tile").click({ force: true }); await page.getByText("XLM").first().click({ force: true }); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); - await page.getByTestId("BackButton").click(); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible(); + await clickVisibleBackButton(page); await expect(page.getByTestId("account-view")).toBeVisible(); await page.getByTestId("nav-link-swap").click(); - await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); + await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible(); const newAmountInput = page.locator('input[type="text"]').first(); await expect(newAmountInput).toHaveValue("0"); @@ -895,6 +969,109 @@ test("Swap resets state when navigating back to account", async ({ await expect(page.getByTestId("swap-src-asset-tile")).toContainText("XLM"); }); +test("Send flow starts at token picker and proceeds to amount screen", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await loginToTestAccount({ page, extensionId, context }); + + await page.getByTestId("nav-link-send").click({ force: true }); + + // Step 1: token picker + await expect(visibleTokenList(page)).toBeVisible(); + await expect(page.getByTestId("send-amount-amount-input")).toHaveCount(0); + + // Step 2: select XLM → DESTINATION + await page.getByTestId("SendRow-native").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); + await expect(page.getByTestId("send-amount-amount-input")).toHaveCount(0); + + // Step 3: fill address and continue → AMOUNT + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); +}); + +test("Send flow from asset detail starts at destination step", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + const stubOverrides = async () => { + await stubAccountBalancesE2e(page); + }; + await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + + await page.getByText("E2E").click(); + await page.getByTestId("asset-detail-send-button").click(); + + // Asset detail pre-selects asset via ?asset= param → starts at DESTINATION + await expect(page.getByTestId("send-to-input")).toBeVisible(); + // Token picker must NOT be visible (asset was pre-selected) + await expect(page.getByTestId("token-list")).toHaveCount(0); + // Amount screen must NOT be visible yet + await expect(page.getByTestId("send-amount-amount-input")).toHaveCount(0); +}); + +test("Send flow change recipient from amount screen dismisses back", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await loginToTestAccount({ page, extensionId, context }); + + // Navigate to AMOUNT screen + await page.getByTestId("nav-link-send").click({ force: true }); + await expect(visibleTokenList(page)).toBeVisible(); + await page.getByTestId("SendRow-native").click(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + // Click address tile to change recipient — DESTINATION slides in from bottom + await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); + + // Back dismisses back to AMOUNT + await clickVisibleBackButton(page); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); +}); + +test("Send flow change token from amount screen dismisses back", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + const stubOverrides = async () => { + await stubAccountBalancesWithUSDC(page); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + await switchToMainnet(page); + + // Navigate to AMOUNT screen + await page.getByTestId("nav-link-send").click({ force: true }); + await expect(visibleTokenList(page)).toBeVisible(); + await page.getByTestId("SendRow-native").click(); + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + // Click token tile to change asset — token picker slides in from bottom + await page.getByTestId("send-amount-edit-dest-asset").click(); + // Should now show token list (no tabs, inline list) + await expect(visibleTokenList(page)).toBeVisible(); + + // Back dismisses back to AMOUNT + await clickVisibleBackButton(page); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); +}); + test.afterAll(async ({ page, extensionId, context }) => { if ( test.info().status !== test.info().expectedStatus && @@ -935,26 +1112,19 @@ test("Send token payment from Asset Detail", async ({ await page.getByText("E2E").click(); await page.getByTestId("asset-detail-send-button").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("0.123"); - - await page.getByTestId("address-tile").click(); + // Asset detail navigates with ?asset= param, so we land at DESTINATION (not token picker) + await expect(page.getByTestId("send-to-input")).toBeVisible(); await page .getByTestId("send-to-input") .fill("GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY"); await page.getByText("Continue").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.123"); await expect(page.getByTestId("send-amount-amount-input")).toHaveValue( "0.123", ); - // confirm that input width stays proportional to amount length - await expect(page.getByTestId("send-amount-amount-input")).toHaveCSS( - "width", - "102px", - ); - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); await reviewSendButton.click({ force: true }); @@ -987,16 +1157,14 @@ test("Send XLM payment from Asset Detail", async ({ await page.getByText("XLM").click(); await page.getByTestId("asset-detail-send-button").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("0.01"); - - await page.getByTestId("address-tile").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); await page .getByTestId("send-to-input") .fill("GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY"); await page.getByText("Continue").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("0.01"); await expect(page.getByTestId("send-amount-amount-input")).toHaveValue( "0.01", ); @@ -1021,3 +1189,345 @@ test.beforeEach(async ({ page }) => { (window as any).IS_PLAYWRIGHT = "true"; }); }); + +// ============================================================================ +// Navigation & Multi-Step Flow Tests +// ============================================================================ +// Tests for send flow navigation, state management, and complex user journeys + +async function goToTokenAmountStepFromHomeSend(page: Page) { + await page.getByTestId("nav-link-send").click({ force: true }); + + const tokenList = page.locator('[data-testid="token-list"]:visible').first(); + const destinationInput = page.getByTestId("send-to-input"); + + await Promise.race([ + tokenList.waitFor({ state: "visible", timeout: 10000 }).catch(() => null), + destinationInput + .first() + .waitFor({ state: "visible", timeout: 10000 }) + .catch(() => null), + ]); + + await expect(page).toHaveURL(/\/account\/sendPayment/); + + if (await tokenList.isVisible()) { + await page.getByTestId("SendRow-native").first().click(); + } + + await expect(destinationInput).toBeVisible({ timeout: 10000 }); + + await destinationInput.fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); +} + +async function goToAssetDetail(page: Page) { + await page.getByText("E2E").first().click({ force: true }); + await expect(page.getByTestId("asset-detail-send-button")).toBeVisible(); +} + +async function goToCollectibleDetail(page: Page) { + await page.getByTestId("account-tab-collectibles").click(); + await page.getByTestId("account-collectible-image").first().click(); + await expect(page.getByTestId("CollectibleDetail")).toBeVisible(); +} + +async function goToCollectibleReviewStep(page: Page) { + // Collectible send may open at destination first depending entry path. + if ((await page.getByTestId("send-to-input").count()) > 0) { + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); + } + + await expect(page.getByTestId("SelectedCollectible")).toBeVisible(); +} + +test("Send flow navigation: home to amount to back returns to account home", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await loginToTestAccount({ page, extensionId, context }); + + // Initiate send from home account screen + await goToTokenAmountStepFromHomeSend(page); + + // Close send flow from amount step + await clickVisibleBackButton(page); + + // Verify return to account home (not token picker) + await expect(page.getByTestId("account-view")).toBeVisible(); + await expect(page.getByTestId("token-list")).toHaveCount(0); +}); + +test("Send flow navigation: collectible selection closes to home account", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await loginToTestAccount({ page, extensionId, context }); + + // Open send flow from home + await page.getByTestId("nav-link-send").click({ force: true }); + await expect(page.getByTestId("token-list")).toBeVisible(); + await expect(page).toHaveURL(/\/account\/sendPayment/); + + // Select a collectible and navigate to details + await page.getByText("Stellar Frog 1").first().click({ force: true }); + await goToCollectibleReviewStep(page); + + // Close from collectible send flow + await clickVisibleBackButton(page); + + // Verify return to account home + await expect(page.getByTestId("account-view")).toBeVisible(); + await expect(page.getByTestId("token-list")).toHaveCount(0); +}); + +test("Send flow navigation: initiate from token detail closes back to token detail", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + const stubOverrides = async () => { + await stubAccountBalancesE2e(page); + }; + await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); + + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + + // Navigate to token detail and initiate send + await goToAssetDetail(page); + await page.getByTestId("asset-detail-send-button").click(); + await expect(page.getByTestId("send-to-input")).toBeVisible(); + await expect(page).toHaveURL(/\/account\/sendPayment/); + + // Enter recipient and proceed to amount + await page.getByTestId("send-to-input").fill(FUNDED_DESTINATION); + await page.getByText("Continue").click({ force: true }); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + // Close send flow and verify return to token detail + await clickVisibleBackButton(page); + await expect(page).toHaveURL(/tab=tokens&asset_detail=/); + await expect(page.getByTestId("asset-detail-send-button")).toBeVisible(); + await expect( + page.getByRole("heading", { name: "E2E", exact: true }), + ).toBeVisible(); +}); + +test("Send flow navigation: initiate from collectible detail closes back to collectible detail", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await loginToTestAccount({ page, extensionId, context }); + + // Navigate to collectible and initiate send + await goToCollectibleDetail(page); + await page.getByTestId("CollectibleDetail__footer__buttons__send").click(); + + // Proceed through collectible send flow + await goToCollectibleReviewStep(page); + await expect(page).toHaveURL(/\/account\/sendPayment/); + + // Close and verify return to collectible detail + await clickVisibleBackButton(page); + + await expect(page).toHaveURL(/tab=collectibles&collection_detail=/); + await expect(page.getByTestId("CollectibleDetail")).toBeVisible(); + await expect( + page.getByTestId("CollectibleDetail__footer__buttons__send"), + ).toBeVisible(); +}); + +// ============================================================================ +// Send Flow Workflows +// ============================================================================ +// Comprehensive integration tests combining navigation, input handling, and submission + +test("Send workflow: navigate, close, re-enter, input, and submit transaction", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await loginToTestAccount({ page, extensionId, context }); + + // First send attempt: navigate to amount, enter value, then close + await goToTokenAmountStepFromHomeSend(page); + await page.getByTestId("send-amount-amount-input").fill("5.5"); + await expect(page.getByTestId("send-amount-amount-input")).toHaveValue("5.5"); + + // Close send flow + await clickVisibleBackButton(page); + await expect(page.getByTestId("account-view")).toBeVisible(); + + // Re-enter send flow - clear previous input + await goToTokenAmountStepFromHomeSend(page); + + // Enter new valid amount (field will auto-clear on new entry) + await page.getByTestId("send-amount-amount-input").fill("1.234567"); + await expect(page.getByTestId("send-amount-amount-input")).toHaveValue( + "1.234567", + ); + + // Proceed to review and submission + const reviewButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewButton).toBeEnabled({ timeout: 10000 }); + await reviewButton.click({ force: true }); + + // Verify reached review screen + await expect(page.getByText("You are sending")).toBeVisible(); + await expect(page.getByTestId("SubmitAction")).toBeVisible({ + timeout: 15000, + }); + await expect(page.getByTestId("SubmitAction")).toBeEnabled(); +}); + +test("Send workflow: input handling with amounts, formatting, and value boundaries", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await loginToTestAccount({ page, extensionId, context }); + + await goToTokenAmountStepFromHomeSend(page); + + // Test 1: High value with thousand separators + await page.getByTestId("send-amount-amount-input").fill("99999999"); + await expect(page.getByTestId("send-amount-amount-input")).toHaveValue( + "99,999,999", + ); + + // Test 2: Non-numeric characters are filtered + await page.getByTestId("send-amount-amount-input").fill("abc123.45xyz"); + await expect(page.getByTestId("send-amount-amount-input")).toHaveValue( + "123.45", + ); + + // Test 3: Negative sign is stripped + await page.getByTestId("send-amount-amount-input").fill("-50"); + await expect(page.getByTestId("send-amount-amount-input")).toHaveValue("50"); + + // Test 4: Valid decimal at boundary (7 decimals - Stellar max precision) + await page.getByTestId("send-amount-amount-input").fill("0.0000001"); + await expect(page.getByTestId("send-amount-amount-input")).toHaveValue( + "0.0000001", + ); + await page.waitForTimeout(500); + + // Test 5: Zero amount disables continue button + await page.getByTestId("send-amount-amount-input").fill("0"); + await page.waitForTimeout(500); + let continueBtn = page.getByTestId("send-amount-btn-continue"); + await expect(continueBtn).toBeDisabled(); + + // Test 6: Valid amount enables continue and reaches review + await page.getByTestId("send-amount-amount-input").fill("2.5"); + await expect(page.getByTestId("send-amount-amount-input")).toHaveValue("2.5"); + continueBtn = page.getByTestId("send-amount-btn-continue"); + await expect(continueBtn).toBeEnabled({ timeout: 10000 }); + await continueBtn.click({ force: true }); + + // Verify transaction review screen + await expect(page.getByText("You are sending")).toBeVisible(); +}); + +test("Send workflow: 25% amount is preserved across fiat toggle and review", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await stubSendTokenPrices(context); + const stubOverrides = async () => { + await stubAccountBalancesWithUSDC(page); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + await switchToMainnet(page); + + await goToTokenAmountStepFromHomeSend(page); + + // Set amount via percentage button and capture exact value shown in token mode. + await page.getByRole("button", { name: "25%" }).click({ force: true }); + const amountInput = page.getByTestId("send-amount-amount-input"); + await expect(amountInput).toBeVisible(); + const amountBeforeToggle = await amountInput.inputValue(); + await expect(amountBeforeToggle).not.toBe("0"); + + // Toggle to fiat and back to token. + const toggleButton = page.locator(".SendAmount__amount-price button").first(); + await expect(toggleButton).toHaveCount(1, { timeout: 15000 }); + await expect(toggleButton).toBeVisible({ timeout: 10000 }); + await toggleButton.click({ force: true }); + await page.waitForTimeout(500); + await toggleButton.click({ force: true }); + await page.waitForTimeout(500); + + // Exact token input should remain unchanged. + await expect(amountInput).toHaveValue(amountBeforeToggle); + + // Review modal should use the same exact token value. + const continueBtn = page.getByTestId("send-amount-btn-continue"); + await expect(continueBtn).toBeEnabled({ timeout: 10000 }); + await continueBtn.click({ force: true }); + + await expect(page.getByText("You are sending")).toBeVisible(); + await expect(page.getByTestId("review-tx-send-amount")).toContainText( + `${amountBeforeToggle.replace(/,/g, "")} XLM`, + ); +}); + +test("Send workflow: typed token amount is preserved across fiat toggle and review", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await stubSendTokenPrices(context); + const stubOverrides = async () => { + await stubAccountBalancesWithUSDC(page); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + await switchToMainnet(page); + + await goToTokenAmountStepFromHomeSend(page); + + const amountInput = page.getByTestId("send-amount-amount-input"); + await expect(amountInput).toBeVisible(); + + await amountInput.fill("11"); + await expect(amountInput).toHaveValue("11"); + + // Toggle to fiat and back. + const toggleButton = page.locator(".SendAmount__amount-price button").first(); + await expect(toggleButton).toHaveCount(1, { timeout: 15000 }); + await expect(toggleButton).toBeVisible({ timeout: 10000 }); + await toggleButton.click({ force: true }); + await page.waitForTimeout(500); + await toggleButton.click({ force: true }); + await page.waitForTimeout(500); + + // Exact typed token amount should be preserved. + await expect(amountInput).toHaveValue("11"); + + // Review modal should receive the preserved exact token amount. + const continueBtn = page.getByTestId("send-amount-btn-continue"); + await expect(continueBtn).toBeEnabled({ timeout: 10000 }); + await continueBtn.click({ force: true }); + + await expect(page.getByText("You are sending")).toBeVisible(); + await expect(page.getByTestId("review-tx-send-amount")).toContainText( + "11 XLM", + ); + await expect(page.getByTestId("SubmitAction")).toBeVisible({ + timeout: 15000, + }); +}); diff --git a/extension/e2e-tests/test-fixtures.ts b/extension/e2e-tests/test-fixtures.ts index 835ddbed45..547b9ac362 100644 --- a/extension/e2e-tests/test-fixtures.ts +++ b/extension/e2e-tests/test-fixtures.ts @@ -14,9 +14,11 @@ export const test = base.extend<{ extensionId: string; page: Page; language: string; + viewportSize: { width: number; height: number } | null; }>({ + viewportSize: null, language: "en", - context: async ({}, use) => { + context: async ({ viewportSize }, use) => { const pathToExtension = path.join(__dirname, "../build"); const context = await chromium.launchPersistentContext("", { headless: false, @@ -25,6 +27,7 @@ export const test = base.extend<{ `--disable-extensions-except=${pathToExtension}`, `--load-extension=${pathToExtension}`, ], + ...(viewportSize ? { viewport: viewportSize } : {}), }); // Mark every page (including popups opened by the extension) as a diff --git a/extension/src/popup/components/InternalTransaction/CollectiblesList/index.tsx b/extension/src/popup/components/InternalTransaction/CollectiblesList/index.tsx index 1d31a1a099..519a09050e 100644 --- a/extension/src/popup/components/InternalTransaction/CollectiblesList/index.tsx +++ b/extension/src/popup/components/InternalTransaction/CollectiblesList/index.tsx @@ -12,7 +12,7 @@ import { getUserCollections } from "popup/helpers/collectibles"; import "./styles.scss"; -/* UI for displaying a vertical list of clickable collectibles */ +/* UI for displaying a vertical list of clickable collectibles grouped by collection */ export const CollectiblesList = ({ collectibles, @@ -42,47 +42,61 @@ export const CollectiblesList = ({ ); } - const flattenedCollections = userCollectibles.flatMap((collection) => { - if (collection.collection) { - return collection.collection.collectibles; - } - return []; - }); - return (