diff --git a/.github/agents/playwright-test-generator.agent.md b/.github/agents/playwright-test-generator.agent.md new file mode 100644 index 0000000000..02ab4b833c --- /dev/null +++ b/.github/agents/playwright-test-generator.agent.md @@ -0,0 +1,104 @@ +--- +name: playwright-test-generator +description: + 'Use this agent when you need to create automated browser tests using + Playwright Examples: Context: User wants to generate a test for the + test plan item. + ' +tools: + - search + - playwright-test/browser_click + - playwright-test/browser_drag + - playwright-test/browser_evaluate + - playwright-test/browser_file_upload + - playwright-test/browser_handle_dialog + - playwright-test/browser_hover + - playwright-test/browser_navigate + - playwright-test/browser_press_key + - playwright-test/browser_select_option + - playwright-test/browser_snapshot + - playwright-test/browser_type + - playwright-test/browser_verify_element_visible + - playwright-test/browser_verify_list_visible + - playwright-test/browser_verify_text_visible + - playwright-test/browser_verify_value + - playwright-test/browser_wait_for + - playwright-test/generator_read_log + - playwright-test/generator_setup_page + - playwright-test/generator_write_test +model: Claude Sonnet 4 +mcp-servers: + playwright-test: + type: stdio + command: npx + args: + - playwright + - run-test-mcp-server + tools: + - "*" +--- + +You are a Playwright Test Generator, an expert in browser automation and +end-to-end testing. Your specialty is creating robust, reliable Playwright tests +that accurately simulate user interactions and validate application behavior. + +# For each test you generate + +- Obtain the test plan with all the steps and verification specification +- Run the `generator_setup_page` tool to set up page for the scenario +- For each step and verification in the scenario, do the following: + - Use Playwright tool to manually execute it in real-time. + - Use the step description as the intent for each Playwright tool call. +- Retrieve generator log via `generator_read_log` +- Immediately after reading the test log, invoke `generator_write_test` with the + generated source code + + - File should contain single test + - File name must be fs-friendly scenario name + - Test must be placed in a describe matching the top-level test plan item + - Test title must match the scenario name + - Includes a comment with the step text before each step execution. Do not + duplicate comments if step requires multiple actions. + - Always use best practices from the log when generating tests. + + + For following plan: + + ```markdown file=specs/plan.md + ### 1. Adding New Todos + + **Seed:** `tests/seed.spec.ts` + + #### 1.1 Add Valid Todo + + **Steps:** + + 1. Click in the "What needs to be done?" input field + + #### 1.2 Add Multiple Todos + + ... + ``` + + Following file is generated: + + ```ts file=add-valid-todo.spec.ts + // spec: specs/plan.md + // seed: tests/seed.spec.ts + + test.describe('Adding New Todos', () => { + test('Add Valid Todo', async { page } => { + // 1. Click in the "What needs to be done?" input field + await page.click(...); + + ... + }); + }); + ``` + + diff --git a/.github/agents/playwright-test-healer.agent.md b/.github/agents/playwright-test-healer.agent.md new file mode 100644 index 0000000000..3f20b13290 --- /dev/null +++ b/.github/agents/playwright-test-healer.agent.md @@ -0,0 +1,76 @@ +--- +name: playwright-test-healer +description: + Use this agent when you need to debug and fix failing Playwright tests +tools: + - search + - edit + - playwright-test/browser_console_messages + - playwright-test/browser_evaluate + - playwright-test/browser_generate_locator + - playwright-test/browser_network_requests + - playwright-test/browser_snapshot + - playwright-test/test_debug + - playwright-test/test_list + - playwright-test/test_run +model: Claude Sonnet 4 +mcp-servers: + playwright-test: + type: stdio + command: npx + args: + - playwright + - run-test-mcp-server + tools: + - "*" +--- + +You are the Playwright Test Healer, an expert test automation engineer +specializing in debugging and resolving Playwright test failures. Your mission +is to systematically identify, diagnose, and fix broken Playwright tests using a +methodical approach. + +Your workflow: + +1. **Initial Execution**: Run all tests using `test_run` tool to identify + failing tests +2. **Debug failed tests**: For each failing test run `test_debug`. +3. **Error Investigation**: When the test pauses on errors, use available + Playwright MCP tools to: + - Examine the error details + - Capture page snapshot to understand the context + - Analyze selectors, timing issues, or assertion failures +4. **Root Cause Analysis**: Determine the underlying cause of the failure by + examining: + - Element selectors that may have changed + - Timing and synchronization issues + - Data dependencies or test environment problems + - Application changes that broke test assumptions +5. **Code Remediation**: Edit the test code to address identified issues, + focusing on: + - Updating selectors to match current application state + - Fixing assertions and expected values + - Improving test reliability and maintainability + - For inherently dynamic data, utilize regular expressions to produce + resilient locators +6. **Verification**: Restart the test after each fix to validate the changes +7. **Iteration**: Repeat the investigation and fixing process until the test + passes cleanly + +Key principles: + +- Be systematic and thorough in your debugging approach +- Document your findings and reasoning for each fix +- Prefer robust, maintainable solutions over quick hacks +- Use Playwright best practices for reliable test automation +- If multiple errors exist, fix them one at a time and retest +- Provide clear explanations of what was broken and how you fixed it +- You will continue this process until the test runs successfully without any + failures or errors. +- If the error persists and you have high level of confidence that the test is + correct, mark this test as test.fixme() so that it is skipped during the + execution. Add a comment before the failing step explaining what is happening + instead of the expected behavior. +- Do not ask user questions, you are not interactive tool, do the most + reasonable thing possible to pass the test. +- Never wait for networkidle or use other discouraged or deprecated apis diff --git a/.github/agents/playwright-test-planner.agent.md b/.github/agents/playwright-test-planner.agent.md new file mode 100644 index 0000000000..09ab4a6733 --- /dev/null +++ b/.github/agents/playwright-test-planner.agent.md @@ -0,0 +1,92 @@ +--- +name: playwright-test-planner +description: + Use this agent when you need to create comprehensive test plan for a web + application or website +tools: + - search + - playwright-test/browser_click + - playwright-test/browser_close + - playwright-test/browser_console_messages + - playwright-test/browser_drag + - playwright-test/browser_evaluate + - playwright-test/browser_file_upload + - playwright-test/browser_handle_dialog + - playwright-test/browser_hover + - playwright-test/browser_navigate + - playwright-test/browser_navigate_back + - playwright-test/browser_network_requests + - playwright-test/browser_press_key + - playwright-test/browser_select_option + - playwright-test/browser_snapshot + - playwright-test/browser_take_screenshot + - playwright-test/browser_type + - playwright-test/browser_wait_for + - playwright-test/planner_setup_page + - playwright-test/planner_save_plan +model: Claude Sonnet 4 +mcp-servers: + playwright-test: + type: stdio + command: npx + args: + - playwright + - run-test-mcp-server + tools: + - "*" +--- + +You are an expert web test planner with extensive experience in quality +assurance, user experience testing, and test scenario design. Your expertise +includes functional testing, edge case identification, and comprehensive test +coverage planning. + +You will: + +1. **Navigate and Explore** + + - Invoke the `planner_setup_page` tool once to set up page before using any + other tools + - Explore the browser snapshot + - Do not take screenshots unless absolutely necessary + - Use `browser_*` tools to navigate and discover interface + - Thoroughly explore the interface, identifying all interactive elements, + forms, navigation paths, and functionality + +2. **Analyze User Flows** + + - Map out the primary user journeys and identify critical paths through the + application + - Consider different user types and their typical behaviors + +3. **Design Comprehensive Scenarios** + + Create detailed test scenarios that cover: + + - Happy path scenarios (normal user behavior) + - Edge cases and boundary conditions + - Error handling and validation + +4. **Structure Test Plans** + + Each scenario must include: + + - Clear, descriptive title + - Detailed step-by-step instructions + - Expected outcomes where appropriate + - Assumptions about starting state (always assume blank/fresh state) + - Success criteria and failure conditions + +5. **Create Documentation** + + Submit your test plan using `planner_save_plan` tool. + +**Quality Standards**: + +- Write steps that are specific enough for any tester to follow +- Include negative testing scenarios +- Ensure scenarios are independent and can be run in any order + +**Output Format**: Always save the complete test plan as a markdown file with +clear headings, numbered steps, and professional formatting suitable for sharing +with development and QA teams. diff --git a/.github/workflows/runTests.yml b/.github/workflows/runTests.yml index e9aad9dd51..7d3301b46c 100644 --- a/.github/workflows/runTests.yml +++ b/.github/workflows/runTests.yml @@ -9,7 +9,7 @@ jobs: test-ci: name: test timeout-minutes: 45 - runs-on: macos-latest + runs-on: macos-latest-xlarge steps: - uses: actions/checkout@v6 with: diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000000..7bd34d3e6d --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,10 @@ +{ + "servers": { + "playwright-test": { + "type": "stdio", + "command": "npx", + "args": ["playwright", "run-test-mcp-server", "--headless"] + } + }, + "inputs": [] +} diff --git a/extension/e2e-tests/README.md b/extension/e2e-tests/README.md new file mode 100644 index 0000000000..c88e60f92a --- /dev/null +++ b/extension/e2e-tests/README.md @@ -0,0 +1,330 @@ +# E2E Tests for Freighter Extension + +This directory contains end-to-end tests for the Freighter wallet extension using Playwright. + +## Prerequisites + +Before running the tests, ensure you have: + +1. **Node.js and Yarn**: Install dependencies from the root of the extension directory +2. **Built Extension**: The extension must be built before running tests + +## Setup + +### 1. Install Dependencies + +From the root directory: + +```bash +yarn install +``` + +### 2. Configure Backend + +If you need to test against a specific backend, create an `.env` file at `extension/.env`: + +``` +INDEXER_URL=INSERT_YOUR_INDEXER_URL_V1 +``` + +``` +INDEXER_V2_URL=INSERT_YOUR_INDEXER_URL_V2 +``` + +### 3. Build the Extension + +The tests run against the built extension in the `extension/build` directory: + +```bash +yarn build:extension +``` + +## Running Tests + +### Run All Tests + +From the root directory: + +```bash +yarn test:e2e +``` + +### Run Specific Test Files + +```bash +yarn test:e2e sendPayment.test.ts +``` + +### Run Tests in UI Mode + +Playwright provides a UI mode for debugging: + +```bash +yarn test:e2e --ui +``` + +### Run Tests in Headed Mode + +To see the browser while tests run: + +```bash +yarn test:e2e --headed +``` + +### Run Specific Tests by Name + +```bash +yarn test:e2e -g "Send doesn't throw error" +``` + +## Test Structure + +### Test Files + +- `accountHistory.test.ts` - Tests for account history and transaction viewing +- `addAsset.test.ts` - Tests for adding custom assets +- `addCollectible.test.ts` - Tests for adding NFTs/collectibles +- `allowList.test.ts` - Tests for domain allowlist functionality +- `buyWithOnramp.test.ts` - Tests for on-ramp integration +- `loadAccount.test.ts` - Tests for loading/importing accounts +- `login.test.ts` - Tests for authentication flows +- `memo.test.ts` - Tests for memo functionality in transactions +- `onboarding.test.ts` - Tests for user onboarding flow +- `sendCollectible.test.ts` - Tests for sending NFTs +- `sendPayment.test.ts` - Tests for payment functionality +- `translations-pt.test.ts` - Tests for Portuguese translations + +### Test Fixtures + +The `test-fixtures.ts` file provides custom fixtures for: + +- Browser context with extension loaded +- Extension ID extraction +- Service worker access +- Language configuration + +### Helpers + +The `helpers/` directory contains: + +- `login.ts` - Authentication helper functions +- `stubs.ts` - API mocking utilities +- `test-token.ts` - Test token constants + +## Test Configuration + +The Playwright configuration is in `extension/playwright.config.ts`: + +- **Timeout**: 15 seconds per test +- **Retries**: 5 attempts on failure +- **Workers**: 8 locally, 4 on CI +- **Browser**: Chromium only +- **Viewport**: 1280x720 + +## Debugging Tests + +### View Test Traces + +When tests fail, traces are automatically captured: + +```bash +npx playwright show-trace test-results/[test-name]/trace.zip +``` + +### Debug Mode + +Run with the Playwright Inspector: + +```bash +PWDEBUG=1 npx playwright test +``` + +### Take Screenshots on Failure + +Screenshots are automatically captured in the `test-results/` directory when tests fail. + +## Integration Mode + +For running tests sequentially (useful for tests that modify shared state): + +```bash +IS_INTEGRATION_MODE=true yarn test:e2e +``` + +## CI + +### GitHub Actions + +e2e tests are run in CI by this GitHub action workflow: https://github.com/stellar/freighter/blob/master/.github/workflows/runTests.yml + +This job runs on every commit on a PR. + +This workflow toggles IS_INTEGRATION_MODE `on` if the branch is pointing at `master` (indicating that we're getting ready to deploy to production) + +### View CI Test Trace + +In CI, if the tests fail, the GitHub Action will upload an artifact that will allow you to view a recording of what happened in the browser in CI. This link will be visible in the test run in the `Run actions/upload-artifact@v5` step. Download this artifact and unzip. + +The failed tests can be viewed using the `show-trace` command: + +```bash +npx playwright show-trace test-results/[test-name]/trace.zip +``` + +## Common Issues + +### Extension Not Loading + +- Ensure the extension is built: `yarn build` +- Check that `extension/build` directory exists + +### Test Timeouts + +- Tests are marked with `test.slow()` for operations that may take longer +- Default timeout is 15 seconds, with 5 retries + +### API Stubs Not Working + +- Check that stub functions are properly defined in test setup +- Ensure routes are registered before navigation + +## Writing New Tests + +### Basic Test Structure + +```typescript +import { test, expect, expectPageToHaveScreenshot } from "./test-fixtures"; +import { loginToTestAccount } from "./helpers/login"; + +test("My test description", async ({ page, extensionId, context }) => { + test.slow(); // If test needs more time + + // loginToTestAccount will automatically stub all API's by default + await loginToTestAccount({ page, extensionId, context }); + + // Your test code here + await page.getByTestId("my-element").click(); + await expect(page.getByText("Expected text")).toBeVisible(); +}); +``` + +### Using Stubs + +```typescript +const stubOverrides = async () => { + await stubAccountBalancesWithUSDC(page); +}; + +await loginToTestAccount({ page, extensionId, context, stubOverrides }); +``` + +## Snapshot Testing + +Snapshot tests capture visual or structural baselines of your application and compare future runs against those baselines. This helps catch unintended visual regressions. + +### Visual Snapshots + +Freighter uses the `expectPageToHaveScreenshot` function for visual snapshot testing: + +```typescript +await expectPageToHaveScreenshot({ + page, + screenshot: "my-feature.png", +}); +``` + +**Snapshot files** are stored in directories alongside test files: + +- `sendPayment.test.ts` → `sendPayment.test.ts-snapshots/` +- `addAsset.test.ts` → `addAsset.test.ts-snapshots/` + +### Updating Snapshots + +When you intentionally change the UI, update snapshots: + +```bash +yarn test:e2e --update-snapshots +``` + +Or update snapshots for a specific test: + +```bash +yarn test:e2e sendPayment.test.ts --update-snapshots +``` + +### Reviewing Snapshot Changes + +When a snapshot test fails, you can review the differences: + +1. **In the test results directory**: + + ``` + test-results/[test-name]/ + ├── expected.png (baseline snapshot) + ├── actual.png (current output) + └── diff.png (visual diff highlighting changes) + ``` + +2. **In VS Code**: Use the "Compare" feature on actual/expected/diff images + +3. **In CI**: Download the test artifacts to review snapshot diffs locally + +### Best Practices + +- **Keep snapshots focused**: Use snapshots to verify specific UI components or pages +- **Review carefully**: Always review snapshot diffs before updating to catch unintended changes +- **Commit snapshots**: Include updated snapshots in your git commits +- **Avoid flaky snapshots**: + - Mock dates/times if they appear in screenshots + - Ensure consistent asset loading states + - Use fixed viewports (Playwright provides 1280x720) +- **Document changes**: Add comments when updating snapshots for intentional UI changes + +### Example Snapshot Test + +```typescript +import { test, expect, expectPageToHaveScreenshot } from "./test-fixtures"; + +test("Send payment review screen matches snapshot", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + const stubOverrides = async () => { + await stubAccountBalancesWithUSDC(page); + }; + + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + + // Navigate to send flow + await page.getByTestId("nav-link-send").click(); + await page.getByTestId("send-amount-amount-input").fill("100"); + await page.getByTestId("address-tile").click(); + await page + .getByTestId("send-to-input") + .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); + await page.getByText("Continue").click(); + + // Capture the payment review screen + await expectPageToHaveScreenshot({ + page, + screenshot: "send-payment-review.png", + }); +}); +``` + +## CI/CD + +Tests run automatically in CI with: + +- 4 parallel workers +- 1-hour global timeout +- Fail-fast on first failure after retries +- `test.only` forbidden in CI + +## Additional Resources + +- [Playwright Documentation](https://playwright.dev/) +- [Freighter Extension README](../README.md) +- [Test Snapshots](./sendPayment.test.ts-snapshots/) - Visual regression test baselines diff --git a/extension/e2e-tests/accountHistory.test.ts b/extension/e2e-tests/accountHistory.test.ts index ebc5cafb52..71f8a5d7b0 100644 --- a/extension/e2e-tests/accountHistory.test.ts +++ b/extension/e2e-tests/accountHistory.test.ts @@ -1,5 +1,5 @@ import { test, expect, expectPageToHaveScreenshot } from "./test-fixtures"; -import { loginAndFund, loginToTestAccount } from "./helpers/login"; +import { loginToTestAccount } from "./helpers/login"; import { TEST_M_ADDRESS } from "./helpers/test-token"; import { stubAccountBalances, stubTokenDetails } from "./helpers/stubs"; import { @@ -11,46 +11,47 @@ import { Memo, } from "stellar-sdk"; -test("View Account History", async ({ page, extensionId }) => { - test.slow(); - await loginAndFund({ page, extensionId }); +test("View Account History", async ({ page, extensionId, context }) => { + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-account-history").click(); + await expectPageToHaveScreenshot({ page, screenshot: "account-history.png", }); }); -test("View failed transaction", async ({ page, extensionId }) => { - await page.route("*/**/account-history/*", async (route) => { - const json = [ - { - amount: "0.0010000", - asset_code: "USDC", - asset_issuer: - "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - asset_type: "credit_alphanum4", - created_at: "2025-03-21T22:28:46Z", - from: "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - id: "164007621169153", - paging_token: "164007621169153", - source_account: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - to: "GCKUVXILBNYS4FDNWCGCYSJBY2PBQ4KAW2M5CODRVJPUFM62IJFH67J2", - transaction_attr: {}, - transaction_hash: - "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5", - transaction_successful: false, - type: "payment", - type_i: 1, - }, - ]; - await route.fulfill({ json }); - }); +test("View failed transaction", async ({ page, extensionId, context }) => { + const stubOverrides = async () => { + await page.route("*/**/account-history/*", async (route) => { + const json = [ + { + amount: "0.0010000", + asset_code: "USDC", + asset_issuer: + "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + asset_type: "credit_alphanum4", + created_at: "2025-03-21T22:28:46Z", + from: "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + id: "164007621169153", + paging_token: "164007621169153", + source_account: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + to: "GCKUVXILBNYS4FDNWCGCYSJBY2PBQ4KAW2M5CODRVJPUFM62IJFH67J2", + transaction_attr: {}, + transaction_hash: + "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5", + transaction_successful: false, + type: "payment", + type_i: 1, + }, + ]; + await route.fulfill({ json }); + }); + }; - test.slow(); - await loginAndFund({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-account-history").click(); await expect(page.getByTestId("history-item-amount-component")).toHaveText( "Mar 21", @@ -69,179 +70,185 @@ test("View failed transaction", async ({ page, extensionId }) => { screenshot: "failed-transaction.png", }); }); -test("Hide create claimable balance spam", async ({ page, extensionId }) => { - await page.route("*/**/account-history/*", async (route) => { - const json = [ - { - amount: "0.0010000", - asset_code: "USDC", - asset_issuer: - "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - asset_type: "credit_alphanum4", - created_at: "2025-03-21T22:28:46Z", - from: "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - id: "164007621169153", - paging_token: "164007621169153", - source_account: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - to: "GCKUVXILBNYS4FDNWCGCYSJBY2PBQ4KAW2M5CODRVJPUFM62IJFH67J2", - transaction_attr: {}, - transaction_hash: - "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5", - transaction_successful: true, - type: "payment", - type_i: 1, - }, - { - amount: "0.0010000", - asset_code: "USDC", - asset_issuer: - "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - asset_type: "credit_alphanum4", - created_at: "2025-03-21T22:28:46Z", - from: "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - id: "164007621169153", - paging_token: "164007621169153", - source_account: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - to: "GCKUVXILBNYS4FDNWCGCYSJBY2PBQ4KAW2M5CODRVJPUFM62IJFH67J2", - transaction_attr: {}, - transaction_hash: - "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5", - transaction_successful: true, - type: "payment", - type_i: 1, - }, - { - amount: "0.0010000", - asset: "USDC", - created_at: "2025-03-19T22:28:46Z", - id: "164007621169153", - paging_token: "164007621169153", - source_account: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - transaction_attr: { - operation_count: 100, - successful: true, +test("Hide create claimable balance spam", async ({ + page, + extensionId, + context, +}) => { + const stubOverrides = async () => { + await page.route("*/**/account-history/*", async (route) => { + const json = [ + { + amount: "0.0010000", + asset_code: "USDC", + asset_issuer: + "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + asset_type: "credit_alphanum4", + created_at: "2025-03-21T22:28:46Z", + from: "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + id: "164007621169153", + paging_token: "164007621169153", + source_account: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + to: "GCKUVXILBNYS4FDNWCGCYSJBY2PBQ4KAW2M5CODRVJPUFM62IJFH67J2", + transaction_attr: {}, + transaction_hash: + "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5", + transaction_successful: true, + type: "payment", + type_i: 1, }, - transaction_hash: - "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5", - transaction_successful: true, - type: "create_claimable_balance", - type_i: 14, - }, - { - amount: "0.0010000", - asset: "USDC", - created_at: "2025-03-18T22:28:46Z", - id: "164007621169153", - paging_token: "164007621169153", - source_account: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - transaction_attr: { - operation_count: 100, - successful: false, + { + amount: "0.0010000", + asset_code: "USDC", + asset_issuer: + "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + asset_type: "credit_alphanum4", + created_at: "2025-03-21T22:28:46Z", + from: "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + id: "164007621169153", + paging_token: "164007621169153", + source_account: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + to: "GCKUVXILBNYS4FDNWCGCYSJBY2PBQ4KAW2M5CODRVJPUFM62IJFH67J2", + transaction_attr: {}, + transaction_hash: + "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5", + transaction_successful: true, + type: "payment", + type_i: 1, }, - transaction_hash: - "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5", - transaction_successful: false, - type: "create_claimable_balance", - type_i: 14, - }, - ]; - await route.fulfill({ json }); - }); + { + amount: "0.0010000", + asset: "USDC", + created_at: "2025-03-19T22:28:46Z", + id: "164007621169153", + paging_token: "164007621169153", + source_account: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + transaction_attr: { + operation_count: 100, + successful: true, + }, + transaction_hash: + "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5", + transaction_successful: true, + type: "create_claimable_balance", + type_i: 14, + }, + { + amount: "0.0010000", + asset: "USDC", + created_at: "2025-03-18T22:28:46Z", + id: "164007621169153", + paging_token: "164007621169153", + source_account: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + transaction_attr: { + operation_count: 100, + successful: false, + }, + transaction_hash: + "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5", + transaction_successful: false, + type: "create_claimable_balance", + type_i: 14, + }, + ]; + await route.fulfill({ json }); + }); + }; - test.slow(); - await loginAndFund({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-account-history").click(); await expect(page.getByTestId("AppHeaderPageTitle")).toHaveText("History"); const historyItems = page.getByTestId("history-item"); - expect(historyItems).toHaveCount(2); + await expect(historyItems).toHaveCount(2); }); test("History row displays muxed address extracted from XDR for payment", async ({ page, extensionId, + context, }) => { - test.slow(); - const TEST_ACCOUNT = - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY"; - const BASE_G_ADDRESS = - "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"; - const TRANSACTION_HASH = - "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5"; + const stubOverrides = async () => { + const TEST_ACCOUNT = + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY"; + const BASE_G_ADDRESS = + "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"; + const TRANSACTION_HASH = + "686601028de9ddf40a1c24461a6a9c0415d60a39255c35eccad0b52ac1e700a5"; + + const sourceKeypair = Keypair.fromSecret( + "SBPQUZ6G4FZNWFHKUWC5BEYWF6R52E3SEP7R3GWYSM2XTKGF5LNTWW4R", + ); + const sourceAccount = { + accountId: () => sourceKeypair.publicKey(), + sequenceNumber: () => "376114581078717", + incrementSequenceNumber: () => {}, + }; + + const tx = new TransactionBuilder(sourceAccount as any, { + fee: "100", + networkPassphrase: Networks.TESTNET, + }) + .addOperation( + Operation.payment({ + destination: TEST_M_ADDRESS, // Muxed address in XDR + asset: Asset.native(), + amount: "1.0000000", + }), + ) + .setTimeout(30) + .build(); + + const envelopeXdr = tx.toXDR(); + // Stub account history BEFORE login to ensure it catches all requests + // (returns both base G address and muxed M address) + await page.route("**/account-history/**", async (route) => { + const json = [ + { + amount: "1.0000000", + asset_code: "XLM", + asset_type: "native", + created_at: "2025-03-21T22:28:46Z", + from: TEST_ACCOUNT, + id: "164007621169153", + paging_token: "164007621169153", + source_account: TEST_ACCOUNT, + to: BASE_G_ADDRESS, // Horizon returns base G address + to_muxed: TEST_M_ADDRESS, // And also the muxed M address + transaction_attr: { + hash: TRANSACTION_HASH, + memo: null, + fee_charged: "100", + operation_count: 1, + }, + transaction_hash: TRANSACTION_HASH, + transaction_successful: true, + type: "payment", + type_i: 1, + }, + ]; + await route.fulfill({ json }); + }); - const sourceKeypair = Keypair.fromSecret( - "SBPQUZ6G4FZNWFHKUWC5BEYWF6R52E3SEP7R3GWYSM2XTKGF5LNTWW4R", - ); - const sourceAccount = { - accountId: () => sourceKeypair.publicKey(), - sequenceNumber: () => "376114581078717", - incrementSequenceNumber: () => {}, + // Stub transaction XDR endpoint + await page.route("**/transactions/**", async (route) => { + const url = route.request().url(); + if (url.includes(TRANSACTION_HASH)) { + await route.fulfill({ + json: { + envelope_xdr: envelopeXdr, + }, + }); + } else { + await route.continue(); + } + }); }; - const tx = new TransactionBuilder(sourceAccount as any, { - fee: "100", - networkPassphrase: Networks.TESTNET, - }) - .addOperation( - Operation.payment({ - destination: TEST_M_ADDRESS, // Muxed address in XDR - asset: Asset.native(), - amount: "1.0000000", - }), - ) - .setTimeout(30) - .build(); - - const envelopeXdr = tx.toXDR(); - - await stubAccountBalances(page); - await loginToTestAccount({ page, extensionId }); - - // Stub account history (returns both base G address and muxed M address) - await page.route("**/account-history/**", async (route) => { - const json = [ - { - amount: "1.0000000", - asset_code: "XLM", - asset_type: "native", - created_at: "2025-03-21T22:28:46Z", - from: TEST_ACCOUNT, - id: "164007621169153", - paging_token: "164007621169153", - source_account: TEST_ACCOUNT, - to: BASE_G_ADDRESS, // Horizon returns base G address - to_muxed: TEST_M_ADDRESS, // And also the muxed M address - transaction_attr: { - hash: TRANSACTION_HASH, - memo: null, - fee_charged: "100", - operation_count: 1, - }, - transaction_hash: TRANSACTION_HASH, - transaction_successful: true, - type: "payment", - type_i: 1, - }, - ]; - await route.fulfill({ json }); - }); - - // Stub transaction XDR endpoint - await page.route("**/transactions/**", async (route) => { - const url = route.request().url(); - if (url.includes(TRANSACTION_HASH)) { - await route.fulfill({ - json: { - envelope_xdr: envelopeXdr, - }, - }); - } else { - await route.continue(); - } - }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-account-history").click(); await expect(page.getByTestId("history-item").nth(0)).toBeVisible({ @@ -263,6 +270,7 @@ test("History row displays muxed address extracted from XDR for payment", async test.skip("History row displays address extracted from XDR for createAccount", async ({ page, extensionId, + context, }) => { test.slow(); const TEST_ACCOUNT = @@ -297,7 +305,7 @@ test.skip("History row displays address extracted from XDR for createAccount", a const envelopeXdr = tx.toXDR(); await stubAccountBalances(page); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.route("**/account-history/**", async (route) => { const json = [ @@ -344,8 +352,8 @@ test.skip("History row displays address extracted from XDR for createAccount", a test("History row displays regular G address when no muxed address in XDR", async ({ page, extensionId, + context, }) => { - test.slow(); const TEST_ACCOUNT = "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY"; const G_ADDRESS = "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"; @@ -378,50 +386,51 @@ test("History row displays regular G address when no muxed address in XDR", asyn const envelopeXdr = tx.toXDR(); - // Stub account history - await page.route("**/account-history/**", async (route) => { - const json = [ - { - amount: "1.0000000", - asset_code: "XLM", - asset_type: "native", - created_at: "2025-03-21T22:28:46Z", - from: TEST_ACCOUNT, - id: "164007621169155", - paging_token: "164007621169155", - source_account: TEST_ACCOUNT, - to: G_ADDRESS, - transaction_attr: { - hash: TRANSACTION_HASH, - memo: "test memo", - fee_charged: "100", - operation_count: 1, + const stubOverrides = async () => { + // Stub account history + await page.route("**/account-history/**", async (route) => { + const json = [ + { + amount: "1.0000000", + asset_code: "XLM", + asset_type: "native", + created_at: "2025-03-21T22:28:46Z", + from: TEST_ACCOUNT, + id: "164007621169155", + paging_token: "164007621169155", + source_account: TEST_ACCOUNT, + to: G_ADDRESS, + transaction_attr: { + hash: TRANSACTION_HASH, + memo: "test memo", + fee_charged: "100", + operation_count: 1, + }, + transaction_hash: TRANSACTION_HASH, + transaction_successful: true, + type: "payment", + type_i: 1, }, - transaction_hash: TRANSACTION_HASH, - transaction_successful: true, - type: "payment", - type_i: 1, - }, - ]; - await route.fulfill({ json }); - }); + ]; + await route.fulfill({ json }); + }); - // Stub transaction XDR endpoint - await page.route("**/transactions/**", async (route) => { - const url = route.request().url(); - if (url.includes(TRANSACTION_HASH)) { - await route.fulfill({ - json: { - envelope_xdr: envelopeXdr, - }, - }); - } else { - await route.continue(); - } - }); + // Stub transaction XDR endpoint + await page.route("**/transactions/**", async (route) => { + const url = route.request().url(); + if (url.includes(TRANSACTION_HASH)) { + await route.fulfill({ + json: { + envelope_xdr: envelopeXdr, + }, + }); + } else { + await route.continue(); + } + }); + }; - await stubAccountBalances(page); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-account-history").click(); await expect(page.getByTestId("history-item").first()).toBeVisible({ @@ -448,46 +457,49 @@ test.describe("Asset Diffs in Transaction History", () => { test("Display single asset diff for received payment", async ({ page, extensionId, + context, }) => { await stubAccountBalances(page); await stubTokenDetails(page); - await page.route("*/**/account-history/*", async (route) => { - const json = [ - { - amount: "100.0000000", - asset_type: "native", - created_at: "2025-03-21T22:28:46Z", - from: COUNTERPARTY, - id: "100000000001", - paging_token: "100000000001", - source_account: COUNTERPARTY, - to: TEST_ACCOUNT, - transaction_hash: - "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", - transaction_successful: true, - type: "payment", - type_i: 1, - asset_balance_changes: [ - { - asset_type: "native", - from: COUNTERPARTY, - to: TEST_ACCOUNT, - amount: "100.0000000", + const stubOverrides = async () => { + await page.route("*/**/account-history/*", async (route) => { + const json = [ + { + amount: "100.0000000", + asset_type: "native", + created_at: "2025-03-21T22:28:46Z", + from: COUNTERPARTY, + id: "100000000001", + paging_token: "100000000001", + source_account: COUNTERPARTY, + to: TEST_ACCOUNT, + transaction_hash: + "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", + transaction_successful: true, + type: "payment", + type_i: 1, + asset_balance_changes: [ + { + asset_type: "native", + from: COUNTERPARTY, + to: TEST_ACCOUNT, + amount: "100.0000000", + }, + ], + transaction_attr: { + hash: "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", + memo: null, + fee_charged: "100", + operation_count: 1, }, - ], - transaction_attr: { - hash: "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", - memo: null, - fee_charged: "100", - operation_count: 1, }, - }, - ]; - await route.fulfill({ json }); - }); + ]; + await route.fulfill({ json }); + }); + }; - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-account-history").click(); await expect(page.getByTestId("history-item").first()).toBeVisible({ @@ -510,60 +522,63 @@ test.describe("Asset Diffs in Transaction History", () => { test("Display both credit and debit for swap operation", async ({ page, extensionId, + context, }) => { await stubAccountBalances(page); await stubTokenDetails(page); - await page.route("*/**/account-history/*", async (route) => { - const json = [ - { - asset_type: "native", - amount: "100.0000000", - created_at: "2025-03-21T22:28:46Z", - from: COUNTERPARTY, - id: "100000000002", - paging_token: "100000000002", - source_account: TEST_ACCOUNT, - to: TEST_ACCOUNT, - transaction_hash: - "swap123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", - transaction_successful: true, - type: "path_payment_strict_receive", - type_i: 2, - source_amount: "50.0000000", - source_asset_type: "credit_alphanum4", - source_asset_code: "USDC", - source_asset_issuer: - "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - asset_balance_changes: [ - { - asset_type: "credit_alphanum4", - asset_code: "USDC", - asset_issuer: - "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - from: TEST_ACCOUNT, - to: COUNTERPARTY, - amount: "50.0000000", + const stubOverrides = async () => { + await page.route("*/**/account-history/*", async (route) => { + const json = [ + { + asset_type: "native", + amount: "100.0000000", + created_at: "2025-03-21T22:28:46Z", + from: COUNTERPARTY, + id: "100000000002", + paging_token: "100000000002", + source_account: TEST_ACCOUNT, + to: TEST_ACCOUNT, + transaction_hash: + "swap123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", + transaction_successful: true, + type: "path_payment_strict_receive", + type_i: 2, + source_amount: "50.0000000", + source_asset_type: "credit_alphanum4", + source_asset_code: "USDC", + source_asset_issuer: + "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + asset_balance_changes: [ + { + asset_type: "credit_alphanum4", + asset_code: "USDC", + asset_issuer: + "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + from: TEST_ACCOUNT, + to: COUNTERPARTY, + amount: "50.0000000", + }, + { + asset_type: "native", + from: COUNTERPARTY, + to: TEST_ACCOUNT, + amount: "100.0000000", + }, + ], + transaction_attr: { + hash: "swap123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", + memo: null, + fee_charged: "150", + operation_count: 1, }, - { - asset_type: "native", - from: COUNTERPARTY, - to: TEST_ACCOUNT, - amount: "100.0000000", - }, - ], - transaction_attr: { - hash: "swap123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", - memo: null, - fee_charged: "150", - operation_count: 1, }, - }, - ]; - await route.fulfill({ json }); - }); + ]; + await route.fulfill({ json }); + }); + }; - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-account-history").click(); await expect(page.getByTestId("history-item").first()).toBeVisible({ @@ -588,69 +603,72 @@ test.describe("Asset Diffs in Transaction History", () => { test("Display multiple asset changes for complex transaction", async ({ page, extensionId, + context, }) => { await stubAccountBalances(page); await stubTokenDetails(page); - await page.route("*/**/account-history/*", async (route) => { - const json = [ - { - asset_type: "native", - amount: "500.0000000", - created_at: "2025-03-21T22:28:46Z", - from: COUNTERPARTY, - id: "100000000003", - paging_token: "100000000003", - source_account: TEST_ACCOUNT, - to: TEST_ACCOUNT, - transaction_hash: - "multi123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", - transaction_successful: true, - type: "path_payment_strict_receive", - type_i: 2, - source_amount: "100.0000000", - source_asset_type: "credit_alphanum4", - source_asset_code: "USDC", - source_asset_issuer: - "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - asset_balance_changes: [ - { - asset_type: "credit_alphanum4", - asset_code: "USDC", - asset_issuer: - "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - from: TEST_ACCOUNT, - to: COUNTERPARTY, - amount: "100.0000000", + const stubOverrides = async () => { + await page.route("*/**/account-history/*", async (route) => { + const json = [ + { + asset_type: "native", + amount: "500.0000000", + created_at: "2025-03-21T22:28:46Z", + from: COUNTERPARTY, + id: "100000000003", + paging_token: "100000000003", + source_account: TEST_ACCOUNT, + to: TEST_ACCOUNT, + transaction_hash: + "multi123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", + transaction_successful: true, + type: "path_payment_strict_receive", + type_i: 2, + source_amount: "100.0000000", + source_asset_type: "credit_alphanum4", + source_asset_code: "USDC", + source_asset_issuer: + "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + asset_balance_changes: [ + { + asset_type: "credit_alphanum4", + asset_code: "USDC", + asset_issuer: + "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + from: TEST_ACCOUNT, + to: COUNTERPARTY, + amount: "100.0000000", + }, + { + asset_type: "native", + from: COUNTERPARTY, + to: TEST_ACCOUNT, + amount: "500.0000000", + }, + { + asset_type: "credit_alphanum4", + asset_code: "AQUA", + asset_issuer: + "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA", + from: COUNTERPARTY, + to: TEST_ACCOUNT, + amount: "250.0000000", + }, + ], + transaction_attr: { + hash: "multi123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", + memo: null, + fee_charged: "200", + operation_count: 1, }, - { - asset_type: "native", - from: COUNTERPARTY, - to: TEST_ACCOUNT, - amount: "500.0000000", - }, - { - asset_type: "credit_alphanum4", - asset_code: "AQUA", - asset_issuer: - "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA", - from: COUNTERPARTY, - to: TEST_ACCOUNT, - amount: "250.0000000", - }, - ], - transaction_attr: { - hash: "multi123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", - memo: null, - fee_charged: "200", - operation_count: 1, }, - }, - ]; - await route.fulfill({ json }); - }); + ]; + await route.fulfill({ json }); + }); + }; - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-account-history").click(); await expect(page.getByTestId("history-item").first()).toBeVisible({ @@ -674,67 +692,68 @@ test.describe("Asset Diffs in Transaction History", () => { test("Display Soroban token with 18 decimals correctly", async ({ page, extensionId, + context, }) => { const TOKEN_CONTRACT = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; - await stubAccountBalances(page); - - await page.route("**/token-details/**", async (route) => { - const url = route.request().url(); - if (url.includes(TOKEN_CONTRACT)) { - await route.fulfill({ - json: { - name: "High Precision Token", - decimals: 18, - symbol: "HPT", - }, - }); - } else { - await route.continue(); - } - }); + const stubOverrides = async () => { + await page.route("**/token-details/**", async (route) => { + const url = route.request().url(); + if (url.includes(TOKEN_CONTRACT)) { + await route.fulfill({ + json: { + name: "High Precision Token", + decimals: 18, + symbol: "HPT", + }, + }); + } else { + await route.continue(); + } + }); - await page.route("*/**/account-history/*", async (route) => { - const json = [ - { - amount: "1000000000000000000", - asset_type: "credit_alphanum12", - asset_code: "HPT", - asset_issuer: TOKEN_CONTRACT, - created_at: "2025-03-21T22:28:46Z", - from: COUNTERPARTY, - id: "100000000004", - paging_token: "100000000004", - source_account: COUNTERPARTY, - to: TEST_ACCOUNT, - transaction_hash: - "token123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", - transaction_successful: true, - type: "payment", - type_i: 1, - asset_balance_changes: [ - { - asset_type: "credit_alphanum12", - asset_code: "HPT", - asset_issuer: TOKEN_CONTRACT, - from: COUNTERPARTY, - to: TEST_ACCOUNT, - amount: "1000000000000000000", + await page.route("*/**/account-history/*", async (route) => { + const json = [ + { + amount: "1000000000000000000", + asset_type: "credit_alphanum12", + asset_code: "HPT", + asset_issuer: TOKEN_CONTRACT, + created_at: "2025-03-21T22:28:46Z", + from: COUNTERPARTY, + id: "100000000004", + paging_token: "100000000004", + source_account: COUNTERPARTY, + to: TEST_ACCOUNT, + transaction_hash: + "token123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", + transaction_successful: true, + type: "payment", + type_i: 1, + asset_balance_changes: [ + { + asset_type: "credit_alphanum12", + asset_code: "HPT", + asset_issuer: TOKEN_CONTRACT, + from: COUNTERPARTY, + to: TEST_ACCOUNT, + amount: "1000000000000000000", + }, + ], + transaction_attr: { + hash: "token123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", + memo: null, + fee_charged: "100", + operation_count: 1, }, - ], - transaction_attr: { - hash: "token123def456ghi789jkl012mno345pqr678stu901vwx234yz567890", - memo: null, - fee_charged: "100", - operation_count: 1, }, - }, - ]; - await route.fulfill({ json }); - }); + ]; + await route.fulfill({ json }); + }); + }; - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-account-history").click(); await expect(page.getByTestId("history-item").first()).toBeVisible({ diff --git a/extension/e2e-tests/accountHistory.test.ts-snapshots/account-history-darwin.png b/extension/e2e-tests/accountHistory.test.ts-snapshots/account-history-darwin.png new file mode 100644 index 0000000000..b849f821b1 Binary files /dev/null and b/extension/e2e-tests/accountHistory.test.ts-snapshots/account-history-darwin.png differ diff --git a/extension/e2e-tests/accountHistory.test.ts-snapshots/failed-transaction-darwin.png b/extension/e2e-tests/accountHistory.test.ts-snapshots/failed-transaction-darwin.png new file mode 100644 index 0000000000..6c4d5de18e Binary files /dev/null and b/extension/e2e-tests/accountHistory.test.ts-snapshots/failed-transaction-darwin.png differ diff --git a/extension/e2e-tests/accountHistory.test.ts-snapshots/failed-transaction-history-item-darwin.png b/extension/e2e-tests/accountHistory.test.ts-snapshots/failed-transaction-history-item-darwin.png new file mode 100644 index 0000000000..79aaa7f880 Binary files /dev/null and b/extension/e2e-tests/accountHistory.test.ts-snapshots/failed-transaction-history-item-darwin.png differ diff --git a/extension/e2e-tests/addAsset.test.ts b/extension/e2e-tests/addAsset.test.ts index 2d259b85d3..fcbe3f8fb3 100644 --- a/extension/e2e-tests/addAsset.test.ts +++ b/extension/e2e-tests/addAsset.test.ts @@ -1,143 +1,64 @@ import { test, expect, expectPageToHaveScreenshot } from "./test-fixtures"; -import { loginAndFund, loginToTestAccount } from "./helpers/login"; -import { TEST_TOKEN_ADDRESS, USDC_TOKEN_ADDRESS } from "./helpers/test-token"; +import { loginToTestAccount } from "./helpers/login"; +import { USDC_TOKEN_ADDRESS } from "./helpers/test-token"; import { stubAccountBalances, stubAccountHistory, stubTokenDetails, stubTokenPrices, + stubAllExternalApis, } from "./helpers/stubs"; -// Helper function to avoid importing from extension source (which causes Node.js module resolution issues) -const truncateString = (str: string, charCount = 4) => - str ? `${str.slice(0, charCount)}…${str.slice(-charCount)}` : ""; - -// Skipping this test because Playwright erroneously is unable to click the "Confirm" button -test.skip("Adding and removing unverified Soroban token", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubTokenDetails(page); - await loginToTestAccount({ page, extensionId }); - - await page.getByTestId("account-options-dropdown").click(); - await page.getByText("Manage assets").click(); - await expect(page.getByText("Your assets")).toBeVisible(); - await expectPageToHaveScreenshot({ - page, - screenshot: "manage-assets-page.png", - }); - await page.getByText("Add an asset").click({ force: true }); - await page.getByTestId("search-asset-input").fill(TEST_TOKEN_ADDRESS); - const notOnLists = page.getByTestId("not-asset-on-list"); - const onLists = page.getByTestId("asset-on-list"); - - // Wait for either to be visible - await Promise.race([ - notOnLists.waitFor({ state: "visible" }), - onLists.waitFor({ state: "visible" }), - ]); +// The page navigation after clicking 'Manage Assets' doesn't complete reliably. +// 'Your assets' text never appears even with long timeouts and waitForLoadState. +test.fixme( + "Adding Soroban verified token", + async ({ page, extensionId, context }) => { + test.slow(); + await loginToTestAccount({ page, extensionId, context }); - if (await notOnLists.isVisible()) { - // Case 1: token is not on your lists - await expect(notOnLists).toHaveText("Not on your lists"); - await expect(page.getByTestId("ManageAssetCode")).toHaveText("E2E"); - await expect(page.getByTestId("ManageAssetRowButton")).toHaveText("Add"); - } else if (await onLists.isVisible()) { - // Case 2: token is already on your lists - await expect(onLists).toHaveText("On your lists"); - await expect(page.getByTestId("ManageAssetCode")).toHaveText( - truncateString(TEST_TOKEN_ADDRESS), - ); + await page.getByTestId("account-options-dropdown").click(); + const manageAssetsText = page.getByText("Manage Assets"); + await expect(manageAssetsText).toBeVisible(); + await manageAssetsText.click({ force: true }); + // Wait for the manage assets page to load + await page.waitForLoadState("networkidle"); + await expect(page.getByText("Your assets")).toBeVisible({ timeout: 10000 }); + await page.getByText("Add an asset").click({ force: true }); + await page.getByTestId("search-asset-input").fill(USDC_TOKEN_ADDRESS); + await expect(page.getByTestId("asset-on-list")).toHaveText("On your lists"); + await expect(page.getByTestId("ManageAssetCode")).toHaveText("USDC"); await expect(page.getByTestId("ManageAssetRowButton")).toHaveText("Add"); - } else { - throw new Error( - "Expected token to be either on or not on lists, but neither was visible", - ); - } - await page.getByTestId("ManageAssetRowButton").click(); - await expect(page.getByTestId("ToggleToken__asset-code")).toHaveText( - "E2E Token", - ); - await expect(page.getByTestId("ToggleToken__asset-add-remove")).toHaveText( - "Add Token", - ); - await page.getByRole("button", { name: "Confirm" }).click(); - await expect( - page.getByTestId("ManageAssetRowButton__ellipsis-E2E"), - ).toBeVisible(); - - // now go back and make sure the asset is displayed in the account view - await page.getByTestId("BackButton").click(); - await page.getByTestId("BackButton").click(); - await expect(page.getByTestId("account-view")).toBeVisible(); - await expect(page.getByText("E2E")).toBeVisible(); - - // now go back and remove this asset - await page.getByTestId("account-options-dropdown").click(); - await page.getByText("Manage assets").click(); - await expect(page.getByText("Your assets")).toBeVisible(); - await expect(page.getByTestId("ManageAssetCode")).toHaveText("E2E"); - await expect(page.getByTestId("ManageAssetDomain")).toHaveText( - "Stellar Network", - ); - await page.getByTestId("ManageAssetRowButton__ellipsis-E2E").click(); - await page.getByText("Remove asset").click(); - await expect(page.getByTestId("ToggleToken__asset-code")).toHaveText( - truncateString(TEST_TOKEN_ADDRESS), - ); - await expect(page.getByTestId("ToggleToken__asset-add-remove")).toHaveText( - "Remove Token", - ); - await page.getByRole("button", { name: "Confirm" }).click(); - await expect( - page.getByText("You have no assets added. Get started by adding an asset."), - ).toBeVisible(); -}); - -// Skipping this test because on Testnet, stellar.expert's asset list is formatter incorrectly -test.skip("Adding Soroban verified token", async ({ page, extensionId }) => { - test.slow(); - await loginToTestAccount({ page, extensionId }); - - await page.getByTestId("account-options-dropdown").click(); - await page.getByText("Manage Assets").click({ force: true }); - - await expect(page.getByText("Your assets")).toBeVisible(); - await page.getByText("Add an asset").click({ force: true }); - await page.getByTestId("search-asset-input").fill(USDC_TOKEN_ADDRESS); - await expect(page.getByTestId("asset-on-list")).toHaveText("On your lists"); - await expect(page.getByTestId("ManageAssetCode")).toHaveText("USDC"); - await expect(page.getByTestId("ManageAssetRowButton")).toHaveText("Add"); - await page.getByTestId("ManageAssetRowButton").click({ force: true }); + await page.getByTestId("ManageAssetRowButton").click({ force: true }); - await expectPageToHaveScreenshot({ - page, - screenshot: "manage-assets-verified-token.png", - }); - await page.getByTestId("ManageAssetRowButton").dispatchEvent("click"); - await expect(page.getByTestId("account-view")).toBeVisible({ - timeout: 30000, - }); + await expectPageToHaveScreenshot({ + page, + screenshot: "manage-assets-verified-token.png", + }); + await page.getByTestId("ManageAssetRowButton").dispatchEvent("click"); + await expect(page.getByTestId("account-view")).toBeVisible({ + timeout: 30000, + }); - await page.getByTestId("account-options-dropdown").click(); - await page.getByText("Manage Assets").click(); - await page.getByTestId("ManageAssetRowButton__ellipsis-USDC").click(); - await page.getByText("Remove asset").click(); + await page.getByTestId("account-options-dropdown").click(); + await page.getByText("Manage Assets").click(); + await page.getByTestId("ManageAssetRowButton__ellipsis-USDC").click(); + await page.getByText("Remove asset").click(); - await expect(page.getByTestId("account-view")).toBeVisible({ - timeout: 30000, - }); -}); + await expect(page.getByTestId("account-view")).toBeVisible({ + timeout: 30000, + }); + }, +); test("Adding token on Futurenet", async ({ page, extensionId, context }) => { + await stubAllExternalApis(page, context); await stubTokenDetails(page); await stubAccountBalances(page); await stubAccountHistory(page); await stubTokenPrices(context); test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("account-options-dropdown").click(); await page.getByText("Settings").click(); @@ -165,53 +86,7 @@ test("Adding token on Futurenet", async ({ page, extensionId, context }) => { await expect(page.getByTestId("search-token-input")).toBeVisible(); }); -// Skipping this test because on Testnet, this now resolves to multiple assets -test.skip("Adding classic asset on Testnet", async ({ page, extensionId }) => { - test.slow(); - await loginAndFund({ page, extensionId }); - - await page.getByTestId("account-options-dropdown").click(); - await page.getByText("Manage assets").click(); - await expect(page.getByText("Your assets")).toBeVisible(); - await page.getByText("Add an asset").click({ force: true }); - await page - .getByTestId("search-asset-input") - .fill("GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"); - await expect(page.getByText("USDC")).toBeVisible(); - - await page.getByTestId("ManageAssetRowButton").click(); - await expect( - page.getByTestId("SignTransaction__TrustlineRow__Asset"), - ).toHaveText("USDC"); - await expect( - page.getByTestId("SignTransaction__TrustlineRow__Type"), - ).toHaveText("Add Trustline"); - await page.getByRole("button", { name: "Confirm" }).click(); - await page.getByText("Done").click(); - await expect( - page.getByTestId("ManageAssetRowButton__ellipsis-USDC"), - ).toBeVisible(); - - // now go back and remove this asset - await page.getByTestId("BackButton").click(); - await expect(page.getByText("Your assets")).toBeVisible(); - await expect(page.getByTestId("ManageAssetCode")).toHaveText("USDC"); - await expect(page.getByTestId("ManageAssetDomain")).toHaveText("centre.io"); - await page.getByTestId("ManageAssetRowButton__ellipsis-USDC").click(); - await page.getByText("Remove asset").click(); - await expect( - page.getByTestId("SignTransaction__TrustlineRow__Asset"), - ).toHaveText("USDC"); - await expect( - page.getByTestId("SignTransaction__TrustlineRow__Type"), - ).toHaveText("Remove Trustline"); - await page.getByRole("button", { name: "Confirm" }).click(); - await page.getByText("Done").click(); - await expect( - page.getByText("You have no assets added. Get started by adding an asset."), - ).toBeVisible(); -}); -test.afterAll(async ({ page, extensionId }) => { +test.afterAll(async ({ page, extensionId, context }) => { if ( process.env.IS_INTEGRATION_MODE && test.info().status !== test.info().expectedStatus && @@ -219,7 +94,7 @@ test.afterAll(async ({ page, extensionId }) => { ) { // remove trustline in cleanup if Adding Soroban verified token test failed test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("account-options-dropdown").click(); await page.getByText("Manage assets").click({ force: true }); diff --git a/extension/e2e-tests/addAsset.test.ts-snapshots/manage-assets-page-darwin.png b/extension/e2e-tests/addAsset.test.ts-snapshots/manage-assets-page-darwin.png new file mode 100644 index 0000000000..590d433c8f Binary files /dev/null and b/extension/e2e-tests/addAsset.test.ts-snapshots/manage-assets-page-darwin.png differ diff --git a/extension/e2e-tests/addCollectible.test.ts b/extension/e2e-tests/addCollectible.test.ts index 04b38df5dc..c3f5643a70 100644 --- a/extension/e2e-tests/addCollectible.test.ts +++ b/extension/e2e-tests/addCollectible.test.ts @@ -7,6 +7,7 @@ import { stubTokenDetails, stubTokenPrices, stubCollectibles, + stubAllExternalApis, } from "./helpers/stubs"; test("Add a collectible to an account", async ({ @@ -18,105 +19,109 @@ test("Add a collectible to an account", async ({ owner: "", contracts: [] as { id: string; token_ids: string[] }[], }; - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await stubCollectibles(page); - await page.route("**/collectibles**", async (route) => { - const postData = JSON.parse(route.request().postData() || "{}"); - const { owner, contracts } = postData as { - owner: string; - contracts: { id: string; token_ids: string[] }[]; - }; + const stubOverrides = async () => { + await stubAllExternalApis(page, context); + await stubTokenDetails(page); + await stubAccountBalances(page); + await stubAccountHistory(page); + await stubTokenPrices(page); + await stubScanDapp(context); + await stubCollectibles(page); - if (owner && contracts.length > 0) { - collectiblesParams = { - owner, - contracts, + await page.route("**/collectibles**", async (route) => { + const postData = JSON.parse(route.request().postData() || "{}"); + const { owner, contracts } = postData as { + owner: string; + contracts: { id: string; token_ids: string[] }[]; }; - } - const json = { - data: { - collections: [ - // Stellar Frogs Collection - { - collection: { - address: - "CCTYMI5ME6NFJC675P2CHNVG467YQJQ5E4TWP5RAPYYNKWK7DIUUDENN", // Using XLM contract address for testing - name: "Stellar Frogs", - symbol: "SFROG", - collectibles: [ - { - owner: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - token_id: "1", - token_uri: "https://nftcalendar.io/tokenMetadata/1", - }, - { - owner: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - token_id: "2", - token_uri: "https://nftcalendar.io/tokenMetadata/2", - }, - { - owner: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - token_id: "3", - token_uri: "https://nftcalendar.io/tokenMetadata/3", - }, - ], + if (owner && contracts.length > 0) { + collectiblesParams = { + owner, + contracts, + }; + } + + const json = { + data: { + collections: [ + // Stellar Frogs Collection + { + collection: { + address: + "CCTYMI5ME6NFJC675P2CHNVG467YQJQ5E4TWP5RAPYYNKWK7DIUUDENN", // Using XLM contract address for testing + name: "Stellar Frogs", + symbol: "SFROG", + collectibles: [ + { + owner: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + token_id: "1", + token_uri: "https://nftcalendar.io/tokenMetadata/1", + }, + { + owner: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + token_id: "2", + token_uri: "https://nftcalendar.io/tokenMetadata/2", + }, + { + owner: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + token_id: "3", + token_uri: "https://nftcalendar.io/tokenMetadata/3", + }, + ], + }, }, - }, - // Soroban Domains Collection - { - collection: { - address: "CCCSorobanDomainsCollection", - name: "Soroban Domains", - symbol: "SDOM", - collectibles: [ - { - owner: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - token_id: "102510", - token_uri: "https://nftcalendar.io/tokenMetadata/102510", - }, - { - owner: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - token_id: "102589", - token_uri: "https://nftcalendar.io/tokenMetadata/102589", - }, - ], + // Soroban Domains Collection + { + collection: { + address: "CCCSorobanDomainsCollection", + name: "Soroban Domains", + symbol: "SDOM", + collectibles: [ + { + owner: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + token_id: "102510", + token_uri: "https://nftcalendar.io/tokenMetadata/102510", + }, + { + owner: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + token_id: "102589", + token_uri: "https://nftcalendar.io/tokenMetadata/102589", + }, + ], + }, }, - }, - // Future Monkeys Collection - { - collection: { - address: "CCCFutureMonkeysCollection", - name: "Future Monkeys", - symbol: "FMONK", - collectibles: [ - { - owner: - "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - token_id: "111", - token_uri: "https://nftcalendar.io/tokenMetadata/111", - }, - ], + // Future Monkeys Collection + { + collection: { + address: "CCCFutureMonkeysCollection", + name: "Future Monkeys", + symbol: "FMONK", + collectibles: [ + { + owner: + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + token_id: "111", + token_uri: "https://nftcalendar.io/tokenMetadata/111", + }, + ], + }, }, - }, - ], - }, - }; - await route.fulfill({ json }); - }); + ], + }, + }; + await route.fulfill({ json }); + }); + }; test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("network-selector-open").click(); await page.getByText("Main Net").click(); diff --git a/extension/e2e-tests/addCollectible.test.ts-snapshots/add-collectible-page-darwin.png b/extension/e2e-tests/addCollectible.test.ts-snapshots/add-collectible-page-darwin.png new file mode 100644 index 0000000000..5e9bcc3a99 Binary files /dev/null and b/extension/e2e-tests/addCollectible.test.ts-snapshots/add-collectible-page-darwin.png differ diff --git a/extension/e2e-tests/allowList.test.ts b/extension/e2e-tests/allowList.test.ts index 679089740e..0c91778bb7 100644 --- a/extension/e2e-tests/allowList.test.ts +++ b/extension/e2e-tests/allowList.test.ts @@ -1,9 +1,9 @@ import { expect, test, expectPageToHaveScreenshot } from "./test-fixtures"; -import { loginAndFund, PASSWORD } from "./helpers/login"; +import { loginToTestAccount, PASSWORD } from "./helpers/login"; -test("View Allow List selector", async ({ page, extensionId }) => { +test("View Allow List selector", async ({ page, extensionId, context }) => { test.slow(); - await loginAndFund({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); // open a second tab and go to docs playground const pageTwo = await page.context().newPage(); @@ -61,7 +61,7 @@ test("View Allow List selector", async ({ page, extensionId }) => { await page.locator("#password-input").fill(PASSWORD); await page.getByText("Create New Address").click(); - await expect(page.getByTestId("not-funded")).toBeVisible({ + await expect(page.getByTestId("account-view")).toBeVisible({ timeout: 10000, }); await page.getByTestId("network-selector-open").click(); diff --git a/extension/e2e-tests/allowList.test.ts-snapshots/allowlist-empty-darwin.png b/extension/e2e-tests/allowList.test.ts-snapshots/allowlist-empty-darwin.png new file mode 100644 index 0000000000..e2e07068a3 Binary files /dev/null and b/extension/e2e-tests/allowList.test.ts-snapshots/allowlist-empty-darwin.png differ diff --git a/extension/e2e-tests/allowList.test.ts-snapshots/allowlist-populated-darwin.png b/extension/e2e-tests/allowList.test.ts-snapshots/allowlist-populated-darwin.png new file mode 100644 index 0000000000..5ec272d7a9 Binary files /dev/null and b/extension/e2e-tests/allowList.test.ts-snapshots/allowlist-populated-darwin.png differ diff --git a/extension/e2e-tests/buyWithOnramp.test.ts b/extension/e2e-tests/buyWithOnramp.test.ts index 941f455e3c..2b2db8e78d 100644 --- a/extension/e2e-tests/buyWithOnramp.test.ts +++ b/extension/e2e-tests/buyWithOnramp.test.ts @@ -1,12 +1,64 @@ import { test, expect, expectPageToHaveScreenshot } from "./test-fixtures"; -import { loginAndFund, loginToTestAccount } from "./helpers/login"; +import { loginToTestAccount } from "./helpers/login"; test("should show add XLM page and open Coinbase", async ({ page, extensionId, + context, }) => { - test.slow(); - await loginAndFund({ page, extensionId }); + const stubOverrides = async () => { + // Override account-balances to return 0 XLM balance + await page.route("**/account-balances/**", async (route) => { + const json = { + balances: { + native: { + token: { + type: "native", + code: "XLM", + }, + total: "0", + available: "0", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "1", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, + }, + }, + }, + isFunded: false, + subentryCount: 0, + error: { + horizon: null, + soroban: null, + }, + }; + await route.fulfill({ json }); + }); + + // Stub /token endpoint + await page.route("**/token", async (route) => { + await route.fulfill({ + json: { + data: { + token: "MWYwZjU5ODEtYjkxOC02YzkwLWI3YzItNWVhZDYyZDQ1M2M0", + }, + }, + }); + }); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("network-selector-open").click(); await page.getByText("Main Net").click(); @@ -31,9 +83,61 @@ test("should show add XLM page and open Coinbase", async ({ test("should show Buy with Coinbase and open Coinbase", async ({ page, extensionId, + context, }) => { - test.slow(); - await loginAndFund({ page, extensionId }); + const stubOverrides = async () => { + // Override account-balances to return 0 XLM balance + await page.route("**/account-balances/**", async (route) => { + const json = { + balances: { + native: { + token: { + type: "native", + code: "XLM", + }, + total: "0", + available: "0", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "1", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, + }, + }, + }, + isFunded: false, + subentryCount: 0, + error: { + horizon: null, + soroban: null, + }, + }; + await route.fulfill({ json }); + }); + + // Stub /token endpoint + await page.route("**/token", async (route) => { + await route.fulfill({ + json: { + data: { + token: "MWYwZjU5ODEtYjkxOC02YzkwLWI3YzItNWVhZDYyZDQ1M2M0", + }, + }, + }); + }); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("network-selector-open").click(); await page.getByText("Main Net").click(); diff --git a/extension/e2e-tests/buyWithOnramp.test.ts-snapshots/add-xlm-page-darwin.png b/extension/e2e-tests/buyWithOnramp.test.ts-snapshots/add-xlm-page-darwin.png new file mode 100644 index 0000000000..fb8b01ba1f Binary files /dev/null and b/extension/e2e-tests/buyWithOnramp.test.ts-snapshots/add-xlm-page-darwin.png differ diff --git a/extension/e2e-tests/buyWithOnramp.test.ts-snapshots/buy-with-coinbase-darwin.png b/extension/e2e-tests/buyWithOnramp.test.ts-snapshots/buy-with-coinbase-darwin.png new file mode 100644 index 0000000000..4a406ca026 Binary files /dev/null and b/extension/e2e-tests/buyWithOnramp.test.ts-snapshots/buy-with-coinbase-darwin.png differ diff --git a/extension/e2e-tests/helpers/allowDapp.ts b/extension/e2e-tests/helpers/allowDapp.ts index c41df6d5fd..f6e92999ee 100644 --- a/extension/e2e-tests/helpers/allowDapp.ts +++ b/extension/e2e-tests/helpers/allowDapp.ts @@ -1,4 +1,4 @@ -import { expect, test } from "../test-fixtures"; +import { expect } from "../test-fixtures"; export const allowDapp = async ({ page }) => { // open a second tab and go to docs playground diff --git a/extension/e2e-tests/helpers/login.ts b/extension/e2e-tests/helpers/login.ts index 3f9fc0bf63..65f55f2259 100644 --- a/extension/e2e-tests/helpers/login.ts +++ b/extension/e2e-tests/helpers/login.ts @@ -1,11 +1,15 @@ import StellarHDWallet from "stellar-hd-wallet"; -import { Page } from "@playwright/test"; +import { Page, BrowserContext } from "@playwright/test"; import { expect } from "../test-fixtures"; +import { stubAllExternalApis } from "./stubs"; const { generateMnemonic } = StellarHDWallet; export const PASSWORD = "My-password123"; +/** + * Creates a new wallet and logs into the extension on Test Net. + */ export const login = async ({ page, extensionId, @@ -45,11 +49,25 @@ export const login = async ({ }); await page.getByTestId("network-selector-open").click(); await page.getByText("Test Net").click(); + + // Wait for account-balances API call with TESTNET network param before clicking + const balancesPromise = page.waitForResponse( + (response) => + response.url().includes("/account-balances/") && + response.url().includes("network=TESTNET"), + ); + + // Wait for the balances API call to complete + await balancesPromise; + await expect(page.getByTestId("account-view")).toBeVisible({ timeout: 10000, }); }; +/** + * Logs in using `login()` and funds the account via Friendbot. + */ export const loginAndFund = async ({ page, extensionId, @@ -58,9 +76,11 @@ export const loginAndFund = async ({ extensionId: string; }) => { await login({ page, extensionId }); + await expect(page.getByTestId("not-funded")).toBeVisible({ timeout: 10000, }); + await page.getByRole("button", { name: "Fund with Friendbot" }).click(); await expect(page.getByTestId("account-assets")).toBeVisible({ @@ -68,14 +88,33 @@ export const loginAndFund = async ({ }); }; +/** + * Logs into a deterministic test account, optionally stubbing external APIs. + * + * @param stubOverrides - Optional function to add custom stub routes after `stubAllExternalApis` runs. + * @param isIntegrationMode - Set true to skip all stubbing for integration tests. + */ export const loginToTestAccount = async ({ page, extensionId, + context, + stubOverrides, + isIntegrationMode = false, }: { page: Page; extensionId: string; + context: BrowserContext; + stubOverrides?: () => Promise; + isIntegrationMode?: boolean; }) => { await page.goto(`chrome-extension://${extensionId}/index.html`); + if (context && !isIntegrationMode) { + // Wait for any background activity to complete + await stubAllExternalApis(page, context); + if (stubOverrides) { + await stubOverrides(); + } + } await page.getByText("I already have a wallet").click(); await page.locator("#new-password-input").fill("My-password123"); diff --git a/extension/e2e-tests/helpers/sendPayment.ts b/extension/e2e-tests/helpers/sendPayment.ts index 5d0072f47d..fa9453f945 100644 --- a/extension/e2e-tests/helpers/sendPayment.ts +++ b/extension/e2e-tests/helpers/sendPayment.ts @@ -1,7 +1,7 @@ -import test from "@playwright/test"; +import test, { Page } from "@playwright/test"; import { expect, expectPageToHaveScreenshot } from "../test-fixtures"; -export const sendXlmPayment = async ({ page }) => { +export const sendXlmPayment = async ({ page }: { page: Page }) => { test.setTimeout(180_000); await page.getByTestId("nav-link-send").click({ force: true }); diff --git a/extension/e2e-tests/helpers/stubs.ts b/extension/e2e-tests/helpers/stubs.ts index 601111b085..c9c07d87ae 100644 --- a/extension/e2e-tests/helpers/stubs.ts +++ b/extension/e2e-tests/helpers/stubs.ts @@ -22,6 +22,261 @@ export const STELLAR_EXPERT_ASSET_LIST_JSON = { ], }; +export const stubUserNotification = async (context: BrowserContext) => { + await context.route("*/**/user-notification", async (route) => { + await route.fulfill({ + json: { enabled: false, message: "" }, + }); + }); +}; + +export const stubFeatureFlags = async (context: BrowserContext) => { + await context.route("*/**/feature-flags", async (route) => { + await route.fulfill({ + json: { useSorobanPublic: true }, + }); + }); +}; + +export const stubSubscriptionAccount = async (context: BrowserContext) => { + await context.route("*/**/subscription/account", async (route) => { + await route.fulfill({ + json: { data: {}, error: null }, + }); + }); +}; + +export const stubStellarAssetList = async (page: Page) => { + await page.route("*/**/testnet/asset-list/**", async (route) => { + await route.fulfill({ json: STELLAR_EXPERT_ASSET_LIST_JSON }); + }); +}; + +export const stubAssetSearch = async (page: Page) => { + await page.route("**/asset?search**", async (route) => { + const json = { + _embedded: { + records: [ + { + asset: + "USDC-GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + num_accounts: 1000, + num_trades: 5000, + num_liquidity_pools: 100, + bidding_liabilities: "1000000", + asking_liabilities: "2000000", + }, + ], + }, + }; + await route.fulfill({ json }); + }); +}; + +export const stubHorizonAccounts = async (page: Page) => { + await page.route("**/accounts/**", async (route) => { + await route.fulfill({ + json: { + id: "GDMDFPJPFH4Z2LLUCNNQT3HVQ2XU2TMZBA6OL37C752WCKU7JZO2S52R", + account_id: "GDMDFPJPFH4Z2LLUCNNQT3HVQ2XU2TMZBA6OL37C752WCKU7JZO2S52R", + sequence: "1234567890", + subentry_count: 0, + last_modified_ledger: 12345, + balances: [ + { + balance: "10000.0000000", + asset_type: "native", + }, + ], + signers: [ + { + weight: 1, + key: "GDMDFPJPFH4Z2LLUCNNQT3HVQ2XU2TMZBA6OL37C752WCKU7JZO2S52R", + type: "ed25519_public_key", + }, + ], + data: {}, + thresholds: { + low_threshold: 0, + med_threshold: 0, + high_threshold: 0, + }, + }, + }); + }); +}; + +export const stubHorizonTransactions = async (page: Page) => { + await page.route("**/horizon/**/transactions", async (route) => { + if (route.request().method() === "POST") { + await route.fulfill({ + json: { + hash: "d7fc8d3f7b9e8c7d6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c", + ledger: 10000, + envelope_xdr: + "AAAAAgAAAABNGU5jYvjfSepLUbRF52FIw18Fm1F76RViTI0pjWF7VAAAAAAAAAQoAAAAAAAAAA==", + result_xdr: "AAAAAAAAACgAAAAAAAAAA==", + }, + }); + } else { + await route.fulfill({ + json: { + _embedded: { records: [] }, + }, + }); + } + }); +}; + +export const stubBackendSimulateTx = async (page: Page) => { + await page.route("**/simulate-tx**", async (route) => { + await route.fulfill({ + json: { + preparedTransaction: + "AAAAAgAAAABNGU5jYvjfSepLUbRF52FIw18Fm1F76RViTI0pjWF7VAAAAZAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAABAAAAAAAAAAAAAA=", + simulationResponse: { + minResourceFee: "100", + cost: { + cpuInsns: "1000", + memBytes: "1000", + }, + latestLedger: "10000", + }, + }, + }); + }); +}; + +export const stubBackendSubmitTx = async (page: Page) => { + await page.route("**/submit-tx**", async (route) => { + await route.fulfill({ + json: { + hash: "a7f2d8c9e1b4f3a6d5c8b7e9f1a2d3c4b5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0", + ledger: 10000, + envelope_xdr: + "AAAAAgAAAABNGU5jYvjfSepLUbRF52FIw18Fm1F76RViTI0pjWF7VAAAAZAAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAABAAAAAAAAAAAAAA=", + result_xdr: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", + }, + }); + }); +}; + +export const stubStellarToml = async (page: Page) => { + await page.route("**/.well-known/stellar.toml", async (route) => { + await route.fulfill({ + body: `FEDERATION_SERVER="https://federation.lobstr.co" +NETWORK_PASSPHRASE="Test SDF Network ; September 2015"`, + contentType: "text/plain", + }); + }); +}; + +export const stubFederation = async (page: Page) => { + await page.route("**/federation**", async (route) => { + await route.fulfill({ + json: { + stellar_address: "freighter.pb*lobstr.co", + account_id: "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF", + }, + }); + }); +}; + +export const stubDefaultAccountBalances = async (page: Page) => { + await page.route("**/account-balances/**", async (route) => { + const json = { + balances: { + native: { + token: { + type: "native", + code: "XLM", + }, + total: "10000.0000000", + available: "10000.0000000", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "1", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, + }, + }, + }, + isFunded: true, + subentryCount: 0, + error: { + horizon: null, + soroban: null, + }, + }; + await route.fulfill({ json }); + }); +}; + +export const stubMercuryTransactions = async (page: Page) => { + await page.route("**/transactions**", async (route) => { + await route.fulfill({ + json: { + _embedded: { records: [] }, + }, + }); + }); +}; + +export const stubSorobanRpc = async (page: Page) => { + await page.route("**/soroban/rpc/**", async (route) => { + await route.fulfill({ + json: { jsonrpc: "2.0", id: "1", result: null }, + }); + }); +}; + +export const stubBackendSettingsEndpoint = async (page: Page) => { + await page.route("**/backend-settings", async (route) => { + await route.fulfill({ + json: { + isSorobanPublicEnabled: true, + isRpcHealthy: true, + userNotification: { enabled: false, message: "" }, + }, + }); + }); +}; + +export const stubLedgerKeysAccounts = async (page: Page) => { + await page.route("**/ledger-keys/accounts/**", async (route) => { + await route.fulfill({ + json: { + data: {}, + error: null, + }, + }); + }); +}; + +export const stubFriendbot = async (page: Page | BrowserContext) => { + await page.route("**/friendbot**", async (route) => { + const json = { + hash: "d7fc8d3f7b9e8c7d6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c", + ledger: 10000, + envelope_xdr: + "AAAAAgAAAABNGU5jYvjfSepLUbRF52FIw18Fm1F76RViTI0pjWF7VAAAAAAAAAQoAAAAAAAAAAENQJiZNGU5jYvjfSepLUbRF52FIw18Fm1F76RViTI0pjWF7VAAACgDAAAAFQAAAAEAAAA=", + result_xdr: "AAAAAAAAACgAAAAAAAAAAQAAAAAD6QAAAAAAAAAA", + }; + await route.fulfill({ json }); + }); +}; + export const stubScanDapp = async (context: BrowserContext) => { await context.route("**/scan-dapp**", async (route) => { const json = { @@ -798,22 +1053,1054 @@ export const stubSimulateTokenTransfer = async ( // Backend returns: { simulationResponse: {...}, preparedTransaction: "..." } const json = { simulationResponse: { - minResourceFee: "100", - results: [], - cost: { - cpuInsns: "100", - memBytes: "100", + _parsed: true, + latestLedger: 60969445, + events: [ + { + _attributes: { + inSuccessfulContractCall: true, + event: { + _attributes: { + ext: { + _switch: 0, + }, + type: { + name: "diagnostic", + value: 2, + }, + body: { + _switch: 0, + _arm: "v0", + _value: { + _attributes: { + topics: [ + { + _switch: { + name: "scvSymbol", + value: 15, + }, + _arm: "sym", + _armType: { + _maxLength: 32, + }, + _value: { + type: "Buffer", + data: [102, 110, 95, 99, 97, 108, 108], + }, + }, + { + _switch: { + name: "scvBytes", + value: 13, + }, + _arm: "bytes", + _armType: { + _maxLength: 4294967295, + }, + _value: { + type: "Buffer", + data: [ + 51, 120, 205, 202, 109, 33, 164, 194, 5, 219, + 69, 26, 124, 216, 96, 210, 220, 233, 172, 131, + 124, 131, 55, 133, 20, 250, 59, 61, 195, 99, + 234, 75, + ], + }, + }, + { + _switch: { + name: "scvSymbol", + value: 15, + }, + _arm: "sym", + _armType: { + _maxLength: 32, + }, + _value: { + type: "Buffer", + data: [116, 114, 97, 110, 115, 102, 101, 114], + }, + }, + ], + data: { + _switch: { + name: "scvVec", + value: 16, + }, + _arm: "vec", + _armType: { + _childType: { + _maxLength: 2147483647, + }, + }, + _value: [ + { + _switch: { + name: "scvAddress", + value: 18, + }, + _arm: "address", + _value: { + _switch: { + name: "scAddressTypeAccount", + value: 0, + }, + _arm: "accountId", + _value: { + _switch: { + name: "publicKeyTypeEd25519", + value: 0, + }, + _arm: "ed25519", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 103, 128, 20, 230, 110, 101, 50, 114, 161, + 182, 112, 3, 7, 114, 137, 146, 71, 205, + 29, 71, 49, 173, 43, 60, 76, 234, 6, 247, + 203, 36, 131, 228, + ], + }, + }, + }, + }, + { + _switch: { + name: "scvAddress", + value: 18, + }, + _arm: "address", + _value: { + _switch: { + name: "scAddressTypeAccount", + value: 0, + }, + _arm: "accountId", + _value: { + _switch: { + name: "publicKeyTypeEd25519", + value: 0, + }, + _arm: "ed25519", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 103, 128, 20, 230, 110, 101, 50, 114, 161, + 182, 112, 3, 7, 114, 137, 146, 71, 205, + 29, 71, 49, 173, 43, 60, 76, 234, 6, 247, + 203, 36, 131, 228, + ], + }, + }, + }, + }, + { + _switch: { + name: "scvI128", + value: 10, + }, + _arm: "i128", + _value: { + _attributes: { + hi: { + _value: "0", + }, + lo: { + _value: "100000", + }, + }, + }, + }, + ], + }, + }, + }, + }, + }, + }, + }, + }, + { + _attributes: { + inSuccessfulContractCall: true, + event: { + _attributes: { + ext: { + _switch: 0, + }, + contractId: { + type: "Buffer", + data: [ + 51, 120, 205, 202, 109, 33, 164, 194, 5, 219, 69, 26, 124, + 216, 96, 210, 220, 233, 172, 131, 124, 131, 55, 133, 20, + 250, 59, 61, 195, 99, 234, 75, + ], + }, + type: { + name: "contract", + value: 1, + }, + body: { + _switch: 0, + _arm: "v0", + _value: { + _attributes: { + topics: [ + { + _switch: { + name: "scvSymbol", + value: 15, + }, + _arm: "sym", + _armType: { + _maxLength: 32, + }, + _value: { + type: "Buffer", + data: [116, 114, 97, 110, 115, 102, 101, 114], + }, + }, + { + _switch: { + name: "scvAddress", + value: 18, + }, + _arm: "address", + _value: { + _switch: { + name: "scAddressTypeAccount", + value: 0, + }, + _arm: "accountId", + _value: { + _switch: { + name: "publicKeyTypeEd25519", + value: 0, + }, + _arm: "ed25519", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 103, 128, 20, 230, 110, 101, 50, 114, 161, + 182, 112, 3, 7, 114, 137, 146, 71, 205, 29, + 71, 49, 173, 43, 60, 76, 234, 6, 247, 203, + 36, 131, 228, + ], + }, + }, + }, + }, + { + _switch: { + name: "scvAddress", + value: 18, + }, + _arm: "address", + _value: { + _switch: { + name: "scAddressTypeAccount", + value: 0, + }, + _arm: "accountId", + _value: { + _switch: { + name: "publicKeyTypeEd25519", + value: 0, + }, + _arm: "ed25519", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 103, 128, 20, 230, 110, 101, 50, 114, 161, + 182, 112, 3, 7, 114, 137, 146, 71, 205, 29, + 71, 49, 173, 43, 60, 76, 234, 6, 247, 203, + 36, 131, 228, + ], + }, + }, + }, + }, + ], + data: { + _switch: { + name: "scvI128", + value: 10, + }, + _arm: "i128", + _value: { + _attributes: { + hi: { + _value: "0", + }, + lo: { + _value: "100000", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + _attributes: { + inSuccessfulContractCall: true, + event: { + _attributes: { + ext: { + _switch: 0, + }, + contractId: { + type: "Buffer", + data: [ + 51, 120, 205, 202, 109, 33, 164, 194, 5, 219, 69, 26, 124, + 216, 96, 210, 220, 233, 172, 131, 124, 131, 55, 133, 20, + 250, 59, 61, 195, 99, 234, 75, + ], + }, + type: { + name: "diagnostic", + value: 2, + }, + body: { + _switch: 0, + _arm: "v0", + _value: { + _attributes: { + topics: [ + { + _switch: { + name: "scvSymbol", + value: 15, + }, + _arm: "sym", + _armType: { + _maxLength: 32, + }, + _value: { + type: "Buffer", + data: [ + 102, 110, 95, 114, 101, 116, 117, 114, 110, + ], + }, + }, + { + _switch: { + name: "scvSymbol", + value: 15, + }, + _arm: "sym", + _armType: { + _maxLength: 32, + }, + _value: { + type: "Buffer", + data: [116, 114, 97, 110, 115, 102, 101, 114], + }, + }, + ], + data: { + _switch: { + name: "scvVoid", + value: 1, + }, + }, + }, + }, + }, + }, + }, + }, + }, + ], + transactionData: { + _data: { + _attributes: { + ext: { + _switch: 0, + }, + resources: { + _attributes: { + footprint: { + _attributes: { + readOnly: [ + { + _switch: { + name: "contractData", + value: 6, + }, + _arm: "contractData", + _value: { + _attributes: { + contract: { + _switch: { + name: "scAddressTypeContract", + value: 1, + }, + _arm: "contractId", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 51, 120, 205, 202, 109, 33, 164, 194, 5, + 219, 69, 26, 124, 216, 96, 210, 220, 233, + 172, 131, 124, 131, 55, 133, 20, 250, 59, + 61, 195, 99, 234, 75, + ], + }, + }, + key: { + _switch: { + name: "scvLedgerKeyContractInstance", + value: 20, + }, + }, + durability: { + name: "persistent", + value: 1, + }, + }, + }, + }, + { + _switch: { + name: "contractCode", + value: 7, + }, + _arm: "contractCode", + _value: { + _attributes: { + hash: { + type: "Buffer", + data: [ + 144, 12, 51, 182, 156, 190, 52, 116, 141, 244, + 68, 63, 118, 129, 119, 90, 2, 179, 5, 153, + 227, 186, 185, 199, 201, 248, 179, 34, 90, 59, + 31, 27, + ], + }, + }, + }, + }, + ], + readWrite: [ + { + _switch: { + name: "contractData", + value: 6, + }, + _arm: "contractData", + _value: { + _attributes: { + contract: { + _switch: { + name: "scAddressTypeContract", + value: 1, + }, + _arm: "contractId", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 51, 120, 205, 202, 109, 33, 164, 194, 5, + 219, 69, 26, 124, 216, 96, 210, 220, 233, + 172, 131, 124, 131, 55, 133, 20, 250, 59, + 61, 195, 99, 234, 75, + ], + }, + }, + key: { + _switch: { + name: "scvVec", + value: 16, + }, + _arm: "vec", + _armType: { + _childType: { + _maxLength: 2147483647, + }, + }, + _value: [ + { + _switch: { + name: "scvSymbol", + value: 15, + }, + _arm: "sym", + _armType: { + _maxLength: 32, + }, + _value: { + type: "Buffer", + data: [66, 97, 108, 97, 110, 99, 101], + }, + }, + { + _switch: { + name: "scvAddress", + value: 18, + }, + _arm: "address", + _value: { + _switch: { + name: "scAddressTypeAccount", + value: 0, + }, + _arm: "accountId", + _value: { + _switch: { + name: "publicKeyTypeEd25519", + value: 0, + }, + _arm: "ed25519", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 103, 128, 20, 230, 110, 101, 50, + 114, 161, 182, 112, 3, 7, 114, 137, + 146, 71, 205, 29, 71, 49, 173, 43, + 60, 76, 234, 6, 247, 203, 36, 131, + 228, + ], + }, + }, + }, + }, + ], + }, + durability: { + name: "persistent", + value: 1, + }, + }, + }, + }, + ], + }, + }, + instructions: 817757, + diskReadBytes: 0, + writeBytes: 148, + }, + }, + resourceFee: { + _value: "93238", + }, + }, + }, }, - transactionData: "", - events: [], - returnValue: "", - auth: [], - footprint: { - readOnly: [], - readWrite: [], + minResourceFee: "93238", + result: { + auth: [ + { + _attributes: { + credentials: { + _switch: { + name: "sorobanCredentialsSourceAccount", + value: 0, + }, + }, + rootInvocation: { + _attributes: { + function: { + _switch: { + name: "sorobanAuthorizedFunctionTypeContractFn", + value: 0, + }, + _arm: "contractFn", + _value: { + _attributes: { + contractAddress: { + _switch: { + name: "scAddressTypeContract", + value: 1, + }, + _arm: "contractId", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 51, 120, 205, 202, 109, 33, 164, 194, 5, 219, + 69, 26, 124, 216, 96, 210, 220, 233, 172, 131, + 124, 131, 55, 133, 20, 250, 59, 61, 195, 99, + 234, 75, + ], + }, + }, + functionName: { + type: "Buffer", + data: [116, 114, 97, 110, 115, 102, 101, 114], + }, + args: [ + { + _switch: { + name: "scvAddress", + value: 18, + }, + _arm: "address", + _value: { + _switch: { + name: "scAddressTypeAccount", + value: 0, + }, + _arm: "accountId", + _value: { + _switch: { + name: "publicKeyTypeEd25519", + value: 0, + }, + _arm: "ed25519", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 103, 128, 20, 230, 110, 101, 50, 114, 161, + 182, 112, 3, 7, 114, 137, 146, 71, 205, + 29, 71, 49, 173, 43, 60, 76, 234, 6, 247, + 203, 36, 131, 228, + ], + }, + }, + }, + }, + { + _switch: { + name: "scvAddress", + value: 18, + }, + _arm: "address", + _value: { + _switch: { + name: "scAddressTypeAccount", + value: 0, + }, + _arm: "accountId", + _value: { + _switch: { + name: "publicKeyTypeEd25519", + value: 0, + }, + _arm: "ed25519", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 103, 128, 20, 230, 110, 101, 50, 114, 161, + 182, 112, 3, 7, 114, 137, 146, 71, 205, + 29, 71, 49, 173, 43, 60, 76, 234, 6, 247, + 203, 36, 131, 228, + ], + }, + }, + }, + }, + { + _switch: { + name: "scvI128", + value: 10, + }, + _arm: "i128", + _value: { + _attributes: { + hi: { + _value: "0", + }, + lo: { + _value: "100000", + }, + }, + }, + }, + ], + }, + }, + }, + subInvocations: [], + }, + }, + }, + }, + ], + retval: { + _switch: { + name: "scvVoid", + value: 1, + }, + }, }, + stateChanges: [ + { + type: "updated", + key: { + _switch: { + name: "contractData", + value: 6, + }, + _arm: "contractData", + _value: { + _attributes: { + contract: { + _switch: { + name: "scAddressTypeContract", + value: 1, + }, + _arm: "contractId", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 51, 120, 205, 202, 109, 33, 164, 194, 5, 219, 69, 26, + 124, 216, 96, 210, 220, 233, 172, 131, 124, 131, 55, + 133, 20, 250, 59, 61, 195, 99, 234, 75, + ], + }, + }, + key: { + _switch: { + name: "scvVec", + value: 16, + }, + _arm: "vec", + _armType: { + _childType: { + _maxLength: 2147483647, + }, + }, + _value: [ + { + _switch: { + name: "scvSymbol", + value: 15, + }, + _arm: "sym", + _armType: { + _maxLength: 32, + }, + _value: { + type: "Buffer", + data: [66, 97, 108, 97, 110, 99, 101], + }, + }, + { + _switch: { + name: "scvAddress", + value: 18, + }, + _arm: "address", + _value: { + _switch: { + name: "scAddressTypeAccount", + value: 0, + }, + _arm: "accountId", + _value: { + _switch: { + name: "publicKeyTypeEd25519", + value: 0, + }, + _arm: "ed25519", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 103, 128, 20, 230, 110, 101, 50, 114, 161, 182, + 112, 3, 7, 114, 137, 146, 71, 205, 29, 71, 49, + 173, 43, 60, 76, 234, 6, 247, 203, 36, 131, 228, + ], + }, + }, + }, + }, + ], + }, + durability: { + name: "persistent", + value: 1, + }, + }, + }, + }, + before: { + _attributes: { + lastModifiedLedgerSeq: 60906681, + data: { + _switch: { + name: "contractData", + value: 6, + }, + _arm: "contractData", + _value: { + _attributes: { + ext: { + _switch: 0, + }, + contract: { + _switch: { + name: "scAddressTypeContract", + value: 1, + }, + _arm: "contractId", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 51, 120, 205, 202, 109, 33, 164, 194, 5, 219, 69, + 26, 124, 216, 96, 210, 220, 233, 172, 131, 124, 131, + 55, 133, 20, 250, 59, 61, 195, 99, 234, 75, + ], + }, + }, + key: { + _switch: { + name: "scvVec", + value: 16, + }, + _arm: "vec", + _armType: { + _childType: { + _maxLength: 2147483647, + }, + }, + _value: [ + { + _switch: { + name: "scvSymbol", + value: 15, + }, + _arm: "sym", + _armType: { + _maxLength: 32, + }, + _value: { + type: "Buffer", + data: [66, 97, 108, 97, 110, 99, 101], + }, + }, + { + _switch: { + name: "scvAddress", + value: 18, + }, + _arm: "address", + _value: { + _switch: { + name: "scAddressTypeAccount", + value: 0, + }, + _arm: "accountId", + _value: { + _switch: { + name: "publicKeyTypeEd25519", + value: 0, + }, + _arm: "ed25519", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 103, 128, 20, 230, 110, 101, 50, 114, 161, + 182, 112, 3, 7, 114, 137, 146, 71, 205, 29, + 71, 49, 173, 43, 60, 76, 234, 6, 247, 203, + 36, 131, 228, + ], + }, + }, + }, + }, + ], + }, + durability: { + name: "persistent", + value: 1, + }, + val: { + _switch: { + name: "scvI128", + value: 10, + }, + _arm: "i128", + _value: { + _attributes: { + hi: { + _value: "0", + }, + lo: { + _value: "109268289", + }, + }, + }, + }, + }, + }, + }, + ext: { + _switch: 0, + }, + }, + }, + after: { + _attributes: { + lastModifiedLedgerSeq: 60906681, + data: { + _switch: { + name: "contractData", + value: 6, + }, + _arm: "contractData", + _value: { + _attributes: { + ext: { + _switch: 0, + }, + contract: { + _switch: { + name: "scAddressTypeContract", + value: 1, + }, + _arm: "contractId", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 51, 120, 205, 202, 109, 33, 164, 194, 5, 219, 69, + 26, 124, 216, 96, 210, 220, 233, 172, 131, 124, 131, + 55, 133, 20, 250, 59, 61, 195, 99, 234, 75, + ], + }, + }, + key: { + _switch: { + name: "scvVec", + value: 16, + }, + _arm: "vec", + _armType: { + _childType: { + _maxLength: 2147483647, + }, + }, + _value: [ + { + _switch: { + name: "scvSymbol", + value: 15, + }, + _arm: "sym", + _armType: { + _maxLength: 32, + }, + _value: { + type: "Buffer", + data: [66, 97, 108, 97, 110, 99, 101], + }, + }, + { + _switch: { + name: "scvAddress", + value: 18, + }, + _arm: "address", + _value: { + _switch: { + name: "scAddressTypeAccount", + value: 0, + }, + _arm: "accountId", + _value: { + _switch: { + name: "publicKeyTypeEd25519", + value: 0, + }, + _arm: "ed25519", + _armType: { + _length: 32, + }, + _value: { + type: "Buffer", + data: [ + 103, 128, 20, 230, 110, 101, 50, 114, 161, + 182, 112, 3, 7, 114, 137, 146, 71, 205, 29, + 71, 49, 173, 43, 60, 76, 234, 6, 247, 203, + 36, 131, 228, + ], + }, + }, + }, + }, + ], + }, + durability: { + name: "persistent", + value: 1, + }, + val: { + _switch: { + name: "scvI128", + value: 10, + }, + _arm: "i128", + _value: { + _attributes: { + hi: { + _value: "0", + }, + lo: { + _value: "109268289", + }, + }, + }, + }, + }, + }, + }, + ext: { + _switch: 0, + }, + }, + }, + }, + ], }, - preparedTransaction: "AAAAAgAAAAC8J8h+HyYyP4AAAAA=", // Mock XDR + preparedTransaction: + "AAAAAgAAAABngBTmbmUycqG2cAMHcomSR80dRzGtKzxM6gb3yySD5AABbJoCjnUGAAABwgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABM3jNym0hpMIF20UafNhg0tzprIN8gzeFFPo7PcNj6ksAAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAAZ4AU5m5lMnKhtnADB3KJkkfNHUcxrSs8TOoG98skg+QAAAASAAAAAAAAAABngBTmbmUycqG2cAMHcomSR80dRzGtKzxM6gb3yySD5AAAAAoAAAAAAAAAAAAAAAAAAYagAAAAAQAAAAAAAAAAAAAAATN4zcptIaTCBdtFGnzYYNLc6ayDfIM3hRT6Oz3DY+pLAAAACHRyYW5zZmVyAAAAAwAAABIAAAAAAAAAAGeAFOZuZTJyobZwAwdyiZJHzR1HMa0rPEzqBvfLJIPkAAAAEgAAAAAAAAAAZ4AU5m5lMnKhtnADB3KJkkfNHUcxrSs8TOoG98skg+QAAAAKAAAAAAAAAAAAAAAAAAGGoAAAAAAAAAABAAAAAAAAAAIAAAAGAAAAATN4zcptIaTCBdtFGnzYYNLc6ayDfIM3hRT6Oz3DY+pLAAAAFAAAAAEAAAAHkAwztpy+NHSN9EQ/doF3WgKzBZnjurnHyfizIlo7HxsAAAABAAAABgAAAAEzeM3KbSGkwgXbRRp82GDS3Omsg3yDN4UU+js9w2PqSwAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAABngBTmbmUycqG2cAMHcomSR80dRzGtKzxM6gb3yySD5AAAAAEADHpdAAAAAAAAAJQAAAAAAAFsNgAAAAA=", }; await route.fulfill({ json, status: 200 }); @@ -927,3 +2214,115 @@ export const stubFeeStats = async (page: Page) => { await route.fulfill({ json }); }); }; + +/** + * Comprehensive stub setup for all external API calls + * Call this early in tests to ensure all external APIs are stubbed + */ +export const stubAllExternalApis = async ( + page: Page, + context: BrowserContext, +) => { + // User notification + await stubUserNotification(context); + + // Feature flags + await stubFeatureFlags(context); + + // Subscription account + await stubSubscriptionAccount(context); + + // Stellar Asset List + await stubStellarAssetList(page); + await stubAssetSearch(page); + + // Token metadata + await stubTokenDetails(page); + await stubTokenPrices(page); + await stubIsSac(page); + + // Horizon endpoints + await stubFeeStats(page); + await stubHorizonAccounts(page); + await stubHorizonTransactions(page); + + // Backend transaction endpoints + await stubBackendSimulateTx(page); + await stubBackendSubmitTx(page); + await stubSimulateTokenTransfer(page); + + // Federated address resolution + await stubStellarToml(page); + await stubFederation(page); + + // Friendbot (testnet funding) + await stubFriendbot(page); + + // Account balances + await stubDefaultAccountBalances(page); + + // Collectibles + await stubCollectibles(page); + + // Mercury/History endpoints + // Note: Tests that need account history should call stubAccountHistory() instead + // to provide their own test data + await stubAccountHistory(page); + await stubMercuryTransactions(page); + + // RPC and Soroban + await stubSorobanRpc(page); + + // Backend settings and health checks + await stubBackendSettingsEndpoint(page); + + // Ledger keys accounts + await stubLedgerKeysAccounts(page); + + // Blockaid scan + await stubScanTx(page); + await stubScanDapp(context); +}; + +export const stubBackendSettings = async (page: Page) => { + await page.route("**/feature-flags", async (route) => { + await route.fulfill({ + json: { + useSorobanPublic: true, + }, + }); + }); + + await page.route("**/user-notification", async (route) => { + await route.fulfill({ + json: { + enabled: false, + message: "", + }, + }); + }); +}; + +/** + * Stub a specific API endpoint with custom response + * Useful for test-specific overrides + */ +export const stubApiEndpoint = async ( + page: Page, + pattern: string, + response: any, +) => { + await page.route(pattern, async (route) => { + await route.fulfill({ json: response }); + }); +}; + +/** + * Abort a specific API endpoint + * Useful for testing error handling + */ +export const abortApiEndpoint = async (page: Page, pattern: string) => { + await page.route(pattern, async (route) => { + await route.abort("failed"); + }); +}; diff --git a/extension/e2e-tests/integration-tests/README.md b/extension/e2e-tests/integration-tests/README.md new file mode 100644 index 0000000000..24b4dd0c54 --- /dev/null +++ b/extension/e2e-tests/integration-tests/README.md @@ -0,0 +1,49 @@ +# Integration Tests + +Tests in this folder are designed to work in **both modes**: + +## Running Tests + +### With Integration Mode OFF (Default) + +```bash +yarn test:e2e integration-tests +``` + +When `IS_INTEGRATION_MODE` is not set, tests use `stubAllExternalApis()` in a `beforeEach` hook to mock all external API calls. This allows tests to run quickly and reliably without depending on external services. + +### With Integration Mode ON + +```bash +IS_INTEGRATION_MODE=true yarn test:e2e integration-tests +``` + +When `IS_INTEGRATION_MODE=true`, the `beforeEach` hook skips stubbing, and tests use real API calls against actual Stellar testnet and external services. This validates that the application works correctly with real data. + +## Test Requirements + +All tests in this folder must pass in **both scenarios**: + +1. **Stubbed Mode** (`IS_INTEGRATION_MODE` not set) + + - External APIs are mocked with consistent test data + - Tests run fast and reliably + - No dependency on external services being available + +2. **Integration Mode** (`IS_INTEGRATION_MODE=true`) + - Tests use real Stellar testnet APIs + - Real fee stats, account balances, and transaction data + - Real network responses and delays + - Account must be funded and in valid state + - Recognize that real data means these tests will inheritly be more flakey than stubbed tests + +## Files + +- `addAssetIntegration.test.ts` - Asset management workflows +- `freighterApiIntegration.test.ts` - Freighter API integration with freighter-api +- `sendIntegration.test.ts` - Payment sending workflows + +## What Should Be Added Here + +- Tests that confirm core flows +- Tests that confirm signing and submitting transactions diff --git a/extension/e2e-tests/integration-tests/addAssetIntegration.test.ts b/extension/e2e-tests/integration-tests/addAssetIntegration.test.ts new file mode 100644 index 0000000000..3c86abfc50 --- /dev/null +++ b/extension/e2e-tests/integration-tests/addAssetIntegration.test.ts @@ -0,0 +1,190 @@ +import { test, expect, expectPageToHaveScreenshot } from "../test-fixtures"; +import { loginToTestAccount } from "../helpers/login"; +import { TEST_TOKEN_ADDRESS } from "../helpers/test-token"; + +// test.beforeEach(async ({ page, context }) => { +// if (!process.env.IS_INTEGRATION_MODE) { +// await stubAllExternalApis(page, context); +// await stubTokenDetails(page); +// } +// }); + +const isIntegrationMode = process.env.IS_INTEGRATION_MODE === "true"; + +test("Adding classic asset on Testnet", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); + + await page.getByTestId("account-options-dropdown").click(); + await page.getByText("Manage assets").click(); + await expect(page.getByText("Your assets")).toBeVisible(); + await page.getByText("Add an asset").click({ force: true }); + await page + .getByTestId("search-asset-input") + .fill("GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"); + // Wait for search results to load + await page.waitForLoadState("networkidle"); + // Get the first ManageAssetCode that contains USDC (there may be multiple assets listed) + await expect( + page.locator('span[data-testid="ManageAssetCode"] >> text=USDC').first(), + ).toBeVisible(); + + await page.getByTestId("ManageAssetRowButton").first().click(); + await expect( + page.getByTestId("SignTransaction__TrustlineRow__Asset"), + ).toHaveText("USDC"); + await expect( + page.getByTestId("SignTransaction__TrustlineRow__Type"), + ).toHaveText("Add Trustline"); + await page.getByRole("button", { name: "Confirm" }).click(); + await expect(page.getByText("Done")).toBeVisible(); + + if (isIntegrationMode) { + // if we're running in integration mode, verify the asset was actually added + await page.getByText("Done").click(); + await expect( + page.getByTestId("ManageAssetRowButton__ellipsis-USDC"), + ).toBeVisible(); + + // now go back and remove this asset + await page.getByTestId("BackButton").click(); + await expect(page.getByText("Your assets")).toBeVisible(); + await expect(page.getByTestId("ManageAssetCode")).toHaveText("USDC"); + await expect(page.getByTestId("ManageAssetDomain")).toHaveText("centre.io"); + await page.getByTestId("ManageAssetRowButton__ellipsis-USDC").click(); + await page.getByText("Remove asset").click(); + await expect( + page.getByTestId("SignTransaction__TrustlineRow__Asset"), + ).toHaveText("USDC"); + await expect( + page.getByTestId("SignTransaction__TrustlineRow__Type"), + ).toHaveText("Remove Trustline"); + await page.getByRole("button", { name: "Confirm" }).click(); + await page.getByText("Done").click(); + await expect( + page.getByText( + "You have no assets added. Get started by adding an asset.", + ), + ).toBeVisible(); + } +}); + +// Helper function to avoid importing from extension source (which causes Node.js module resolution issues) +const truncateString = (str: string, charCount = 4) => + str ? `${str.slice(0, charCount)}…${str.slice(-charCount)}` : ""; + +// Snapshot file doesn't exist - run with --update-snapshots to create baseline +test("Adding and removing unverified Soroban token", async ({ + page, + extensionId, + context, +}) => { + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); + + await page.getByTestId("account-options-dropdown").click(); + await page.getByText("Manage assets").click(); + await expect(page.getByText("Your assets")).toBeVisible(); + await expectPageToHaveScreenshot({ + page, + screenshot: "manage-assets-page.png", + }); + await page.getByText("Add an asset").click({ force: true }); + await page.getByTestId("search-asset-input").fill(TEST_TOKEN_ADDRESS); + const notOnLists = page.getByTestId("not-asset-on-list"); + const onLists = page.getByTestId("asset-on-list"); + + // Wait for either to be visible + await Promise.race([ + notOnLists.waitFor({ state: "visible" }), + onLists.waitFor({ state: "visible" }), + ]); + + if (await notOnLists.isVisible()) { + // Case 1: token is not on your lists + await expect(notOnLists).toHaveText("Not on your lists"); + await expect(page.getByTestId("ManageAssetCode")).toHaveText("E2E"); + await expect(page.getByTestId("ManageAssetRowButton")).toHaveText("Add"); + } else if (await onLists.isVisible()) { + // Case 2: token is already on your lists + await expect(onLists).toHaveText("On your lists"); + await expect(page.getByTestId("ManageAssetCode")).toHaveText( + truncateString(TEST_TOKEN_ADDRESS), + ); + await expect(page.getByTestId("ManageAssetRowButton")).toHaveText("Add"); + } else { + throw new Error( + "Expected token to be either on or not on lists, but neither was visible", + ); + } + await page.getByTestId("ManageAssetRowButton").click(); + await expect(page.getByTestId("ToggleToken__asset-code")).toHaveText( + "E2E Token", + ); + await expect(page.getByTestId("ToggleToken__asset-add-remove")).toHaveText( + "Add Token", + ); + await page.getByRole("button", { name: "Confirm" }).click(); + + if (isIntegrationMode) { + // if we're running in integration mode, verify the asset was actually added + await expect( + page.getByTestId("ManageAssetRowButton__ellipsis-E2E"), + ).toBeVisible(); + + // now go back and make sure the asset is displayed in the account view + await page.getByTestId("BackButton").click(); + await page.getByTestId("BackButton").click(); + await expect(page.getByTestId("account-view")).toBeVisible(); + await expect(page.getByText("E2E")).toBeVisible(); + + // now go back and remove this asset + await page.getByTestId("account-options-dropdown").click(); + await page.getByText("Manage assets").click(); + await expect(page.getByText("Your assets")).toBeVisible(); + await expect(page.getByTestId("ManageAssetCode")).toHaveText("E2E"); + await expect(page.getByTestId("ManageAssetDomain")).toHaveText( + "Stellar Network", + ); + await page.getByTestId("ManageAssetRowButton__ellipsis-E2E").click(); + await page.getByText("Remove asset").click(); + await expect(page.getByTestId("ToggleToken__asset-code")).toHaveText( + truncateString(TEST_TOKEN_ADDRESS), + ); + await expect(page.getByTestId("ToggleToken__asset-add-remove")).toHaveText( + "Remove Token", + ); + await page.getByRole("button", { name: "Confirm" }).click(); + await expect( + page.getByText( + "You have no assets added. Get started by adding an asset.", + ), + ).toBeVisible(); + } +}); + +test.afterAll(async ({ page, extensionId, context }) => { + if ( + isIntegrationMode && + test.info().status !== test.info().expectedStatus && + test.info().title === "Adding classic asset on Testnet" + ) { + // remove trustline in cleanup if Adding Soroban verified token test failed + test.slow(); + await loginToTestAccount({ page, extensionId, context }); + + await page.getByTestId("account-options-dropdown").click(); + await page.getByText("Manage assets").click(); + + await page.getByTestId("ManageAssetRowButton__ellipsis-USDC").click(); + await page.getByText("Remove asset").click(); + await page.getByRole("button", { name: "Confirm" }).click(); + await page.getByText("Done").click(); + await expect(page.getByTestId("account-view")).toBeVisible({ + timeout: 30000, + }); + } +}); diff --git a/extension/e2e-tests/integration-tests/addAssetIntegration.test.ts-snapshots/manage-assets-page-chromium-darwin.png b/extension/e2e-tests/integration-tests/addAssetIntegration.test.ts-snapshots/manage-assets-page-chromium-darwin.png new file mode 100644 index 0000000000..590d433c8f Binary files /dev/null and b/extension/e2e-tests/integration-tests/addAssetIntegration.test.ts-snapshots/manage-assets-page-chromium-darwin.png differ diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts similarity index 81% rename from extension/e2e-tests/freighterApiIntegration.test.ts rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts index f1bd04ff9f..c5f5e674b6 100644 --- a/extension/e2e-tests/freighterApiIntegration.test.ts +++ b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts @@ -1,15 +1,8 @@ -import { expect, test, expectPageToHaveScreenshot } from "./test-fixtures"; -import { TEST_TOKEN_ADDRESS } from "./helpers/test-token"; -import { loginToTestAccount } from "./helpers/login"; -import { allowDapp } from "./helpers/allowDapp"; -import { - stubAccountBalances, - stubAccountHistory, - stubIsSac, - stubScanDapp, - stubTokenDetails, - stubTokenPrices, -} from "./helpers/stubs"; +import { expect, test, expectPageToHaveScreenshot } from "../test-fixtures"; +import { TEST_TOKEN_ADDRESS } from "../helpers/test-token"; +import { loginToTestAccount } from "../helpers/login"; +import { allowDapp } from "../helpers/allowDapp"; +import { stubAccountBalances, stubIsSac } from "../helpers/stubs"; const TX_TO_SIGN = "AAAAAgAAAADLvQoIbFw9k0tgjZoOrLTuJJY9kHFYp/YAEAlt/xirbAAAAGQAAAfjAAAOpQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAABngBTmbmUycqG2cAMHcomSR80dRzGtKzxM6gb3yySD5AAAAAAAAAAAAvrwgAAAAAAAAAAA"; @@ -45,18 +38,14 @@ const JSON_MSG_TO_SIGN = JSON.stringify({ const JSON_SIGNED_MSG = '\"42IH7/mvkAT+ltbEG8oEPhVBzP7hb6NU+P+WZP3j1AIMdbwuFPrzBuRFRvLjXdXl5lDmC7aL0zrZIUrfrMXHDw==\"'; +const isIntegrationMode = process.env.IS_INTEGRATION_MODE === "true"; + test("should sign transaction when allowed", async ({ page, extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); await allowDapp({ page }); // open a second tab and go to docs playground @@ -76,6 +65,93 @@ test("should sign transaction when allowed", async ({ await pageTwo.getByText("Sign Transaction XDR").click(); const txPopup = await txPopupPromise; + await stubAccountBalances(txPopup); + // Stub scan-tx with detailed asset diffs + await txPopup.route("**/scan-tx", async (route) => { + await route.fulfill({ + json: { + data: { + simulation: { + status: "Success", + assets_diffs: { + GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY: [ + { + asset: { + type: "NATIVE", + code: "XLM", + }, + in: null, + out: { + usd_price: 0, + summary: "Sent 5 XLM", + value: 5, + raw_value: 50000000, + }, + asset_type: "NATIVE", + }, + ], + GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF: [ + { + asset: { + type: "NATIVE", + code: "XLM", + }, + in: { + usd_price: 0, + summary: "Received 5 XLM", + value: 5, + raw_value: 50000000, + }, + out: null, + asset_type: "NATIVE", + }, + ], + }, + exposures: {}, + assets_ownership_diff: {}, + address_details: [], + account_summary: { + account_assets_diffs: [ + { + asset: { + type: "NATIVE", + code: "XLM", + }, + in: null, + out: { + usd_price: 0, + summary: "Sent 5 XLM", + value: 5, + raw_value: 50000000, + }, + asset_type: "NATIVE", + }, + ], + account_exposures: [], + account_ownerships_diff: [], + total_usd_diff: { + in: 0, + out: 0, + total: 0, + }, + total_usd_exposure: {}, + }, + transaction_actions: null, + }, + validation: { + status: "Success", + result_type: "Benign", + description: "", + reason: "", + classification: "", + features: [], + }, + request_id: "9e460857-734b-405e-9e1f-86e656def1dd", + }, + error: null, + }, + }); + }); await expect(txPopup.getByText("Confirm Transaction")).toBeVisible(); @@ -96,13 +172,7 @@ test.skip("should sign transaction for a specific account when allowed", async ( extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await allowDapp({ page }); // open a second tab and go to docs playground @@ -138,8 +208,9 @@ test.skip("should sign transaction for a specific account when allowed", async ( test("should not sign transaction when not allowed", async ({ page, extensionId, + context, }) => { - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); // open a second tab and go to docs playground const pageTwo = await page.context().newPage(); @@ -158,6 +229,7 @@ test("should not sign transaction when not allowed", async ({ await pageTwo.getByText("Sign Transaction XDR").click(); const txPopup = await txPopupPromise; + await stubAccountBalances(txPopup); await expect( txPopup.getByText( @@ -176,13 +248,7 @@ test("should sign auth entry when allowed", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); await allowDapp({ page }); // open a second tab and go to docs playground @@ -224,13 +290,7 @@ test("should not sign auth entry when not allowed", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); // open a second tab and go to docs playground const pageTwo = await page.context().newPage(); @@ -260,13 +320,7 @@ test("should sign auth entry for a selected account when allowed", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); await allowDapp({ page }); @@ -291,9 +345,10 @@ test("should sign auth entry for a selected account when allowed", async ({ .getByRole("textbox") .nth(2) .fill("GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY"); - await pageTwo.getByText("Sign Authorization Entry XDR").click(); const popupPromise = page.context().waitForEvent("page"); + await pageTwo.getByText("Sign Authorization Entry XDR").click(); + const popup = await popupPromise; await expect(popup.getByText("Confirm Authorization").first()).toBeVisible(); @@ -313,13 +368,7 @@ test("should sign message string when allowed", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); await allowDapp({ page }); // open a second tab and go to docs playground @@ -357,13 +406,7 @@ test("should sign message long string when allowed", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await allowDapp({ page }); // open a second tab and go to docs playground @@ -401,13 +444,7 @@ test("should sign message json when allowed", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); await allowDapp({ page }); // open a second tab and go to docs playground @@ -455,13 +492,7 @@ test("should sign message for a specific account when allowed", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); await allowDapp({ page }); // open a second tab and go to docs playground @@ -483,9 +514,10 @@ test("should sign message for a specific account when allowed", async ({ .getByRole("textbox") .nth(2) .fill("GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY"); - await pageTwo.getByText("Sign Message").click(); const popupPromise = page.context().waitForEvent("page"); + await pageTwo.getByText("Sign Message").click(); + const popup = await popupPromise; await expect(popup.getByText("Sign message")).toBeVisible(); @@ -505,12 +537,7 @@ test("should not sign message when not allowed", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); // open a second tab and go to docs playground const pageTwo = await page.context().newPage(); @@ -535,14 +562,9 @@ test("should add token when allowed", async ({ extensionId, context, }) => { - await stubTokenDetails(context); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); await stubIsSac(context); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); await allowDapp({ page }); // open a second tab and go to docs playground @@ -577,14 +599,11 @@ test("should not add token when not allowed", async ({ extensionId, context, }) => { - await stubTokenDetails(context); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await stubIsSac(context); + if (!isIntegrationMode) { + await stubIsSac(context); + } - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); // open a second tab and go to docs playground const pageTwo = await page.context().newPage(); @@ -618,13 +637,7 @@ test("should get public key when logged out", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); await page.getByTestId("account-options-dropdown").click(); await page.getByText("Settings").click(); await page.getByText("Log Out").click(); diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/add-token-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/add-token-chromium-darwin.png similarity index 100% rename from extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/add-token-chromium-darwin.png rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/add-token-chromium-darwin.png diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/add-token-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/add-token-darwin.png new file mode 100644 index 0000000000..8a41bc00f3 Binary files /dev/null and b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/add-token-darwin.png differ diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-add-token-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-add-token-chromium-darwin.png similarity index 100% rename from extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-add-token-chromium-darwin.png rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-add-token-chromium-darwin.png diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-add-token-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-add-token-darwin.png new file mode 100644 index 0000000000..91af42af5d Binary files /dev/null and b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-add-token-darwin.png differ diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-chromium-darwin.png similarity index 100% rename from extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-chromium-darwin.png rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-chromium-darwin.png diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-message-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-message-chromium-darwin.png similarity index 100% rename from extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-message-chromium-darwin.png rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-message-chromium-darwin.png diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-transaction-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-transaction-chromium-darwin.png similarity index 100% rename from extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-transaction-chromium-darwin.png rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-transaction-chromium-darwin.png diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-transaction-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-transaction-darwin.png new file mode 100644 index 0000000000..427df16304 Binary files /dev/null and b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/domain-not-allowed-sign-transaction-darwin.png differ diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/sign-auth-entry-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-auth-entry-chromium-darwin.png similarity index 100% rename from extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/sign-auth-entry-chromium-darwin.png rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-auth-entry-chromium-darwin.png diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-auth-entry-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-auth-entry-darwin.png new file mode 100644 index 0000000000..4a989f8c54 Binary files /dev/null and b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-auth-entry-darwin.png differ diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/sign-message-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-chromium-darwin.png similarity index 100% rename from extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/sign-message-chromium-darwin.png rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-chromium-darwin.png diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-darwin.png new file mode 100644 index 0000000000..aca7aba939 Binary files /dev/null and b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-darwin.png differ diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/sign-message-json-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-json-chromium-darwin.png similarity index 100% rename from extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/sign-message-json-chromium-darwin.png rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-json-chromium-darwin.png diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-json-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-json-darwin.png new file mode 100644 index 0000000000..6eb629e795 Binary files /dev/null and b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-json-darwin.png differ diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/sign-message-long-string-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-long-string-chromium-darwin.png similarity index 100% rename from extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/sign-message-long-string-chromium-darwin.png rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-long-string-chromium-darwin.png diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-long-string-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-long-string-darwin.png new file mode 100644 index 0000000000..345e14cb9d Binary files /dev/null and b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-message-long-string-darwin.png differ diff --git a/extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/sign-transaction-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-transaction-chromium-darwin.png similarity index 100% rename from extension/e2e-tests/freighterApiIntegration.test.ts-snapshots/sign-transaction-chromium-darwin.png rename to extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-transaction-chromium-darwin.png diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-transaction-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-transaction-darwin.png new file mode 100644 index 0000000000..8134356cac Binary files /dev/null and b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/sign-transaction-darwin.png differ diff --git a/extension/e2e-tests/integration-tests/sendIntegration.test.ts b/extension/e2e-tests/integration-tests/sendIntegration.test.ts new file mode 100644 index 0000000000..66d1e3cdd5 --- /dev/null +++ b/extension/e2e-tests/integration-tests/sendIntegration.test.ts @@ -0,0 +1,443 @@ +import { TransactionBuilder } from "stellar-sdk"; +import { test, expect } from "../test-fixtures"; +import { loginToTestAccount } from "../helpers/login"; +import { TEST_M_ADDRESS, TEST_TOKEN_ADDRESS } from "../helpers/test-token"; + +const isIntegrationMode = process.env.IS_INTEGRATION_MODE === "true"; + +test("Send persists inputs and submits to network", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + let isScanSkiped = false; + + page.on("request", (request) => { + if ( + request + .url() + .includes("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF") + ) { + isScanSkiped = request.url().includes("should_skip_scan=true"); + } + }); + + const stubOverrides = async () => { + await page.route("**/submit-tx", async (route) => { + const json = { + memo: "test memo", + max_fee: "900", + envelope_xdr: + "AAAAAgAAAADLvQoIbFw9k0tgjZoOrLTuJJY9kHFYp/YAEAlt/xirbAAAA4QAAAUVAAAAvgAAAAEAAAAAAAAAAAAAAABper1qAAAAAQAAAAl0ZXN0IG1lbW8AAAAAAAABAAAAAAAAAAEAAAAAZ4AU5m5lMnKhtnADB3KJkkfNHUcxrSs8TOoG98skg+QAAAAAAAAAAACYloAAAAAAAAAAAf8Yq2wAAABApUEUNnMHzyHA+ZclMfxsX1vv5wfoKegPYhxYnOuiSgit7kCLrVcahgbHAnvb0H+SM0PlZwOxEuaeBJA/B7GdAg==", + + successful: true, + }; + await route.fulfill({ json }); + }); + }; + + await loginToTestAccount({ + page, + extensionId, + context, + stubOverrides, + 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 page + .getByTestId("send-to-input") + .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); + + await page.getByText("Continue").click(); + expect(isScanSkiped).toBeTruthy(); + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Add memo + await page.getByTestId("send-amount-btn-memo").click(); + await page.getByTestId("edit-memo-input").fill("test memo"); + await page.getByText("Save").click(); + + // Wait for memo editor to close + await expect(page.getByTestId("edit-memo-input")).not.toBeVisible({ + timeout: 5000, + }); + + // Add fee + await page.getByTestId("send-amount-btn-fee").click(); + await page.getByTestId("edit-tx-settings-fee-input").fill("0.00009"); + await page.getByText("Save").click(); + + // Wait for fee editor to close + await expect(page.getByTestId("edit-tx-settings-fee-input")).not.toBeVisible({ + timeout: 5000, + }); + + // Wait for simulation to complete + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 30000 }); + + // Click Review Send button + await reviewSendButton.click(); + + // Verify review modal opens + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 30000, + }); + await expect(page.getByTestId("review-tx-send-amount")).toHaveText("1 XLM"); + await expect(page.getByTestId("review-tx-memo")).toHaveText("test memo"); + await expect(page.getByTestId("review-tx-fee")).toHaveText("0.00009 XLM"); + + let submitTxResponse = ""; + page.on("response", async (response) => { + if (response.url().includes("/submit-tx")) { + submitTxResponse = await response.text(); + } + }); + + await page.getByTestId(`SubmitAction`).click(); + + await expect(page.getByText("Sent!")).toBeVisible({ + timeout: 60000, + }); + const submitTxResponseJson = JSON.parse(submitTxResponse); + + expect(submitTxResponseJson.memo).toBe("test memo"); + expect(submitTxResponseJson.max_fee).toBe("900"); + + const tx = TransactionBuilder.fromXDR( + submitTxResponseJson.envelope_xdr, + "Test SDF Network ; September 2015", + ); + + const txOp = tx.operations[0] as any; + + expect(txOp.type).toBe("payment"); + expect(txOp.amount).toBe("1.0000000"); + expect(txOp.destination).toBe( + "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF", + ); + expect(txOp.asset.code).toBe("XLM"); + + await page.getByText("Done").click(); +}); + +test("Send XLM payments to recent federated addresses", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + 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 page.getByTestId("send-to-input").fill("freighter.pb*lobstr.co"); + await expect(page.getByTestId("send-to-identicon")).toBeVisible(); + await page.getByText("Continue").click({ force: true }); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + const reviewSendButton = page.getByText("Review Send"); + await expect(reviewSendButton).toBeEnabled({ timeout: 15000 }); + await reviewSendButton.click({ force: true }); + + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 60000, + }); + const submitAction = page.getByTestId("SubmitAction"); + + await page.waitForTimeout(300); + await submitAction.waitFor({ state: "visible" }); + await submitAction.click({ force: true }); + + await expect(page.getByText("Sent!")).toBeVisible({ + timeout: 60000, + }); + + 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 page.getByTestId("address-tile").click(); + + await expect(page.getByText("Recents")).toBeVisible(); + + await page.getByTestId("recent-address-button").click(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + const reviewSendButton2 = page.getByText("Review Send"); + await expect(reviewSendButton2).toBeEnabled({ timeout: 15000 }); + await reviewSendButton2.click(); + + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 60000, + }); + + await page.waitForTimeout(300); + await submitAction.waitFor({ state: "visible" }); + await submitAction.click({ force: true }); + + let accountBalancesRequestWasMade = false; + page.on("request", (request) => { + if (request.url().includes("/account-balances/")) { + accountBalancesRequestWasMade = true; + } + }); + + await expect(page.getByText("Sent!")).toBeVisible({ + timeout: 60000, + }); + expect(accountBalancesRequestWasMade).toBeTruthy(); +}); + +test("Send XLM payment to C address", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + 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 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.getByTestId("send-amount-amount-input").fill(".001"); + await page.getByText("Review Send").click({ force: true }); + + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 60000, + }); + + const submitAction = page.getByTestId("SubmitAction"); + + await page.waitForTimeout(300); + await submitAction.waitFor({ state: "visible" }); + await submitAction.click({ force: true, timeout: 60000 }); + + let accountBalancesRequestWasMade = false; + page.on("request", (request) => { + if (request.url().includes("/account-balances/")) { + accountBalancesRequestWasMade = true; + } + }); + + await expect(page.getByText("Sent!")).toBeVisible({ + timeout: 60000, + }); + expect(accountBalancesRequestWasMade).toBeTruthy(); +}); + +test("Send XLM payment to M address", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + 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 page.getByTestId("send-to-input").fill(TEST_M_ADDRESS); + await page.getByText("Continue").click(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill(".001"); + + const reviewSendButton = page.getByText("Review Send"); + await expect(reviewSendButton).toBeEnabled({ timeout: 15000 }); + await reviewSendButton.click(); + + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 60000, + }); + + const submitButton = page.getByTestId("SubmitAction"); + await submitButton.scrollIntoViewIfNeeded(); + await submitButton.click(); + + let accountBalancesRequestWasMade = false; + page.on("request", (request) => { + if (request.url().includes("/account-balances/")) { + accountBalancesRequestWasMade = true; + } + }); + + await expect(page.getByText("Sent!")).toBeVisible({ + timeout: 60000, + }); + + expect(accountBalancesRequestWasMade).toBeTruthy(); +}); + +test("Send token payment to C address", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + const stubOverrides = async () => { + await page.route("**/account-balances/**", async (route) => { + const json = { + balances: { + native: { + token: { + type: "native", + code: "XLM", + }, + total: "10000.0000000", + available: "10000.0000000", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "1", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, + }, + }, + [`E2E:${TEST_TOKEN_ADDRESS}`]: { + token: { + code: "E2E", + issuer: { + key: TEST_TOKEN_ADDRESS, + }, + }, + contractId: TEST_TOKEN_ADDRESS, + total: "500.0000000", + available: "500.0000000", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "0.5", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, + }, + }, + }, + isFunded: true, + subentryCount: 0, + error: { + horizon: null, + soroban: null, + }, + }; + await route.fulfill({ json }); + }); + }; + await loginToTestAccount({ + page, + extensionId, + context, + stubOverrides, + isIntegrationMode, + }); + + if (isIntegrationMode) { + // in integration mode, make sure the token is added first + await page.getByTestId("account-options-dropdown").click(); + await page.getByText("Manage assets").click(); + await expect(page.getByText("Your assets")).toBeVisible(); + await page.getByText("Add an asset").click({ force: true }); + await page.getByTestId("search-asset-input").fill(TEST_TOKEN_ADDRESS); + await page.getByTestId("ManageAssetRowButton").click(); + await expect(page.getByTestId("ToggleToken__asset-code")).toHaveText( + "E2E Token", + ); + await expect(page.getByTestId("ToggleToken__asset-add-remove")).toHaveText( + "Add Token", + ); + await page.getByRole("button", { name: "Confirm" }).click(); + await expect( + page.getByTestId("ManageAssetRowButton__ellipsis-E2E"), + ).toBeVisible(); + + // now go back and make sure the asset is displayed in the account view + await page.getByTestId("BackButton").click(); + await page.getByTestId("BackButton").click(); + await expect(page.getByTestId("account-view")).toBeVisible(); + } + + 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 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.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 }); + + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 60000, + }); + + const submitButton = page.getByTestId("SubmitAction"); + await submitButton.scrollIntoViewIfNeeded(); + await submitButton.click(); + + let accountBalancesRequestWasMade = false; + page.on("request", (request) => { + if (request.url().includes("/account-balances/")) { + accountBalancesRequestWasMade = true; + } + }); + + await expect(page.getByText("Sent!")).toBeVisible({ + timeout: 60000, + }); + + expect(accountBalancesRequestWasMade).toBeTruthy(); +}); diff --git a/extension/e2e-tests/loadAccount.test.ts b/extension/e2e-tests/loadAccount.test.ts index 9bb61ba74c..995d3173b1 100644 --- a/extension/e2e-tests/loadAccount.test.ts +++ b/extension/e2e-tests/loadAccount.test.ts @@ -15,14 +15,7 @@ test("Load accounts on standalone network", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("account-options-dropdown").click(); await page.getByText("Settings").click(); await page.getByText("Network").click(); @@ -56,13 +49,15 @@ test.skip("Switches account and fetches correct balances while clearing cache", extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); + const stubOverrides = async () => { + await stubTokenDetails(page); + await stubAccountHistory(page); + await stubTokenPrices(page); + await stubScanDapp(context); + }; test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await expect(page.getByTestId("account-assets")).toContainText("XLM"); const account1XlmBalance = await page .getByTestId("asset-amount") @@ -132,93 +127,89 @@ test("Switches network and fetches correct balances while clearing cache", async extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await page.route("**/account-balances/**", async (route) => { - let json = {}; - - if (route.request().url().includes("TESTNET")) { - json = { - balances: { - native: { - token: { - type: "native", - code: "XLM", - }, - total: "2", - available: "2", - sellingLiabilities: "0", - buyingLiabilities: "0", - minimumBalance: "1", - blockaidData: { - result_type: "Benign", - malicious_score: "0.0", - attack_types: {}, - chain: "stellar", - address: "", - metadata: { - type: "", + const stubOverrides = async () => { + await page.route("**/account-balances/**", async (route) => { + let json = {}; + + if (route.request().url().includes("TESTNET")) { + json = { + balances: { + native: { + token: { + type: "native", + code: "XLM", + }, + total: "2", + available: "2", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "1", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, }, - fees: {}, - features: [], - trading_limits: {}, - financial_stats: {}, }, }, - }, - isFunded: true, - subentryCount: 0, - error: { - horizon: null, - soroban: null, - }, - }; - } else { - json = { - balances: { - native: { - token: { - type: "native", - code: "XLM", - }, - total: "1", - available: "1", - sellingLiabilities: "0", - buyingLiabilities: "0", - minimumBalance: "1", - blockaidData: { - result_type: "Benign", - malicious_score: "0.0", - attack_types: {}, - chain: "stellar", - address: "", - metadata: { - type: "", + isFunded: true, + subentryCount: 0, + error: { + horizon: null, + soroban: null, + }, + }; + } else { + json = { + balances: { + native: { + token: { + type: "native", + code: "XLM", + }, + total: "1", + available: "1", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "1", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, }, - fees: {}, - features: [], - trading_limits: {}, - financial_stats: {}, }, }, - }, - isFunded: true, - subentryCount: 0, - error: { - horizon: null, - soroban: null, - }, - }; - } + isFunded: true, + subentryCount: 0, + error: { + horizon: null, + soroban: null, + }, + }; + } - await route.fulfill({ json }); - }); + await route.fulfill({ json }); + }); + }; - test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await expect(page.getByTestId("account-assets")).toContainText("XLM"); await expect(page.getByTestId("asset-amount")).toHaveText("2"); @@ -277,14 +268,7 @@ test("Account Balances should be loaded once and cached", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); let accountBalancesRequestWasMade = false; page.on("request", (request) => { @@ -304,14 +288,7 @@ test("Switches account without password prompt", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await expect(page.getByTestId("account-assets")).toContainText("XLM"); await page.getByTestId("account-view-account-name").click(); await page.getByText("Account 2").click(); @@ -327,16 +304,8 @@ test("Can't change settings on a stale window", async ({ extensionId, context, }) => { - await stubTokenDetails(context); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - test.slow(); - const pageOne = await page.context().newPage(); - await loginToTestAccount({ page: pageOne, extensionId }); + await loginToTestAccount({ page: pageOne, extensionId, context }); // open a second tab and change the account const pageTwo = await page.context().newPage(); @@ -387,7 +356,7 @@ test.skip("Clears cache and fetches balances if it's been 2 minutes since the la test.slow(); await page.clock.install({ time: new Date("2024-01-01T12:00:00") }); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await expect(page.getByTestId("account-assets")).toContainText("XLM"); const account1XlmBalance = await page .getByTestId("asset-amount") @@ -465,11 +434,6 @@ test("Loads wallets data and token prices on Mainnet in batches", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - let tokenPricesCallCount = 0; page.on("request", (request) => { @@ -478,42 +442,86 @@ test("Loads wallets data and token prices on Mainnet in batches", async ({ } }); - await page.route("**/account-balances/**", async (route) => { - const json = { - balances: { - native: { - token: { - type: "native", - code: "XLM", + const stubOverrides = async () => { + await page.route("**/account-balances/**", async (route) => { + const json = { + balances: { + native: { + token: { + type: "native", + code: "XLM", + }, + total: "5", + available: "4", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "1", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, + }, }, - total: "5", - available: "4", - sellingLiabilities: "0", - buyingLiabilities: "0", - minimumBalance: "1", - blockaidData: { - result_type: "Benign", - malicious_score: "0.0", - attack_types: {}, - chain: "stellar", - address: "", - metadata: { - type: "", + "BTC:GCK3D3V2XNLLKRFGFFFDEJXA4O2J4X36HET2FE446AV3M4U7DPHO3PEM": { + token: { + code: "BTC", + issuer: { + key: "GCK3D3V2XNLLKRFGFFFDEJXA4O2J4X36HET2FE446AV3M4U7DPHO3PEM", + }, + }, + symbol: "BTC", + total: "3", + available: "100000099976", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, }, - fees: {}, - features: [], - trading_limits: {}, - financial_stats: {}, }, }, - "BTC:GCK3D3V2XNLLKRFGFFFDEJXA4O2J4X36HET2FE446AV3M4U7DPHO3PEM": { + isFunded: true, + subentryCount: 0, + error: { + horizon: null, + soroban: null, + }, + } as any; + if ( + route + .request() + .url() + .includes("GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY") + ) { + json.balances.native.total = "10"; + json.balances.native.available = "9"; + json.balances[ + "USDC:GCK3D3V2XNLLKRFGFFFDEJXA4O2J4X36HET2FE446AV3M4U7DPHO3PEM" + ] = { token: { - code: "BTC", + code: "USDC", issuer: { key: "GCK3D3V2XNLLKRFGFFFDEJXA4O2J4X36HET2FE446AV3M4U7DPHO3PEM", }, }, - symbol: "BTC", + symbol: "USDC", total: "3", available: "100000099976", blockaidData: { @@ -530,110 +538,66 @@ test("Loads wallets data and token prices on Mainnet in batches", async ({ trading_limits: {}, financial_stats: {}, }, - }, - }, - isFunded: true, - subentryCount: 0, - error: { - horizon: null, - soroban: null, - }, - } as any; - if ( - route - .request() - .url() - .includes("GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY") - ) { - json.balances.native.total = "10"; - json.balances.native.available = "9"; - json.balances[ - "USDC:GCK3D3V2XNLLKRFGFFFDEJXA4O2J4X36HET2FE446AV3M4U7DPHO3PEM" - ] = { - token: { - code: "USDC", - issuer: { - key: "GCK3D3V2XNLLKRFGFFFDEJXA4O2J4X36HET2FE446AV3M4U7DPHO3PEM", - }, - }, - symbol: "USDC", - total: "3", - available: "100000099976", - blockaidData: { - result_type: "Benign", - malicious_score: "0.0", - attack_types: {}, - chain: "stellar", - address: "", - metadata: { - type: "", - }, - fees: {}, - features: [], - trading_limits: {}, - financial_stats: {}, - }, - }; - } - if ( - route - .request() - .url() - .includes("GCKUVXILBNYS4FDNWCGCYSJBY2PBQ4KAW2M5CODRVJPUFM62IJFH67J2") - ) { - json.balances.native.total = "11"; - json.balances.native.available = "10"; - } - if ( - route - .request() - .url() - .includes("GDPXC35MQCSFJCEDNWUJZYDJWKNGNL2YMFZZS47LT46CFVGAJ6GWYGJC") - ) { - json.balances.native.total = "12"; - json.balances.native.available = "11"; - } - if ( - route - .request() - .url() - .includes("GC32JSRAZA6BJYIKEI2X5LAVJKFVDVVIYWRVCAI7Q7N36WKXVDHTGTQA") - ) { - json.balances.native.total = "13"; - json.balances.native.available = "9"; - } - if ( - route - .request() - .url() - .includes("GDY4ZZLCUEBZPHTKSAESUHV37JA5MZ7BGP2KS32DSBSHS7ONSBFZBQ7C") - ) { - json.balances.native.total = "14"; - json.balances.native.available = "9"; - } - if ( - route - .request() - .url() - .includes("GBW2O3KT5E6W6XN2T66X5TM6EXVTTKVHQJE426PUX5ANMGQVLS2KOU3T") - ) { - json.balances.native.total = "15"; - json.balances.native.available = "9"; - } - if ( - route - .request() - .url() - .includes("GARHHLBEZLF3WWIENRZLHKOB62IIDZX2DHNWXAR2E7KVQTDQGDI4H6NU") - ) { - json.balances.native.total = "16"; - json.balances.native.available = "9"; - } - await route.fulfill({ json }); - }); - - test.slow(); - await loginToTestAccount({ page, extensionId }); + }; + } + if ( + route + .request() + .url() + .includes("GCKUVXILBNYS4FDNWCGCYSJBY2PBQ4KAW2M5CODRVJPUFM62IJFH67J2") + ) { + json.balances.native.total = "11"; + json.balances.native.available = "10"; + } + if ( + route + .request() + .url() + .includes("GDPXC35MQCSFJCEDNWUJZYDJWKNGNL2YMFZZS47LT46CFVGAJ6GWYGJC") + ) { + json.balances.native.total = "12"; + json.balances.native.available = "11"; + } + if ( + route + .request() + .url() + .includes("GC32JSRAZA6BJYIKEI2X5LAVJKFVDVVIYWRVCAI7Q7N36WKXVDHTGTQA") + ) { + json.balances.native.total = "13"; + json.balances.native.available = "9"; + } + if ( + route + .request() + .url() + .includes("GDY4ZZLCUEBZPHTKSAESUHV37JA5MZ7BGP2KS32DSBSHS7ONSBFZBQ7C") + ) { + json.balances.native.total = "14"; + json.balances.native.available = "9"; + } + if ( + route + .request() + .url() + .includes("GBW2O3KT5E6W6XN2T66X5TM6EXVTTKVHQJE426PUX5ANMGQVLS2KOU3T") + ) { + json.balances.native.total = "15"; + json.balances.native.available = "9"; + } + if ( + route + .request() + .url() + .includes("GARHHLBEZLF3WWIENRZLHKOB62IIDZX2DHNWXAR2E7KVQTDQGDI4H6NU") + ) { + json.balances.native.total = "16"; + json.balances.native.available = "9"; + } + await route.fulfill({ json }); + }); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("network-selector-open").click(); await page.getByText("Main Net").click(); await expect(page.getByTestId("account-assets")).toContainText("XLM"); @@ -661,12 +625,7 @@ test("Loads wallets data and token prices on Mainnet in batches", async ({ }); test("Renames wallets", async ({ page, extensionId, context }) => { - await stubTokenDetails(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("account-view-account-name").click(); await expect(page.getByText("Wallets")).toBeVisible(); @@ -683,15 +642,10 @@ test("Loads collectibles data with successful metadata", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await stubCollectibles(page, true); - - test.slow(); - await loginToTestAccount({ page, extensionId }); + const stubOverrides = async () => { + await stubCollectibles(page, true); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("account-tab-collectibles").click(); await expect(page.getByText("Stellar Frogs")).toBeVisible(); await expect(page.getByText("Soroban Domains")).toBeVisible(); @@ -831,15 +785,10 @@ test("Loads collectibles data with unsuccessful metadata", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await stubCollectiblesUnsuccessfulMetadata(page); - - test.slow(); - await loginToTestAccount({ page, extensionId }); + const stubOverrides = async () => { + await stubCollectiblesUnsuccessfulMetadata(page); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("account-tab-collectibles").click(); await expect(page.getByText("Stellar Frogs")).toBeVisible(); const counts = await page diff --git a/extension/e2e-tests/loadAccount.test.ts-snapshots/network-form-page-darwin.png b/extension/e2e-tests/loadAccount.test.ts-snapshots/network-form-page-darwin.png new file mode 100644 index 0000000000..0a23a36d27 Binary files /dev/null and b/extension/e2e-tests/loadAccount.test.ts-snapshots/network-form-page-darwin.png differ diff --git a/extension/e2e-tests/login.test.ts b/extension/e2e-tests/login.test.ts index d2f6f03ede..83b3cdd70d 100644 --- a/extension/e2e-tests/login.test.ts +++ b/extension/e2e-tests/login.test.ts @@ -4,9 +4,10 @@ import { test, expect } from "./test-fixtures"; test("Login shows error state on bad password", async ({ page, extensionId, + context, }) => { test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("account-options-dropdown").click(); await page.getByText("Settings").click(); await page.getByText("Log Out").click(); diff --git a/extension/e2e-tests/memo.test.ts b/extension/e2e-tests/memo.test.ts new file mode 100644 index 0000000000..1a3643826b --- /dev/null +++ b/extension/e2e-tests/memo.test.ts @@ -0,0 +1,802 @@ +import { test, expect } from "./test-fixtures"; +import { loginToTestAccount } from "./helpers/login"; +import { TEST_M_ADDRESS, TEST_TOKEN_ADDRESS } from "./helpers/test-token"; +import { + stubAccountBalances, + stubAccountBalancesE2e, + stubAccountHistory, + stubContractSpec, + stubMemoRequiredAccounts, + stubSimulateTokenTransfer, + stubTokenDetails, + stubTokenPrices, + stubAllExternalApis, +} from "./helpers/stubs"; + +const MEMO_REQUIRED_ADDRESS = + "GA6SXIZIKLJHCZI2KEOBEUUOFMM4JUPPM2UTWX6STAWT25JWIEUFIMFF"; + +test.beforeEach(async ({ page, context }) => { + await stubAllExternalApis(page, context); +}); + +test("Send payment shows memo required warning when destination requires memo", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await stubMemoRequiredAccounts(page, 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 page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); + await page.getByText("Continue").click(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Click Review Send to trigger memo validation + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open - this happens after simulation completes + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 30000, + }); + + // Wait for the review transaction content to be ready (not in loading state) + // Check that AddMemoAction is visible (validation complete) + await expect(page.getByTestId("AddMemoAction")).toBeVisible({ + timeout: 15000, + }); + await expect(page.getByText("Add Memo")).toBeVisible(); +}); + +test("Send payment allows submission after adding memo to memo-required address", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await stubMemoRequiredAccounts(page, 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 page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); + await page.getByText("Continue").click(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Add memo from SendPayment page + await page.getByTestId("send-amount-btn-memo").click(); + await page.getByTestId("edit-memo-input").fill("test memo"); + await page.getByText("Save").click(); + + // Verify memo was saved and review modal doesn't auto-open + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + expect(await page.getByText("You are sending").isVisible()).toBeFalsy(); + + // Click Review Send - this triggers simulation and opens review sheet + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + + // Wait for button to be enabled (simulation not in progress) before clicking + await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + + // Click the button to start simulation and open review sheet + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open - this happens after simulation completes + // The simulation may take time, so we wait for the review sheet content + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Wait for the review transaction content to be ready (not in loading state) + // Check that either SubmitAction or AddMemoAction is visible (validation complete) + await expect( + page + .getByTestId("SubmitAction") + .or(page.getByTestId("AddMemoAction")) + .first(), + ).toBeVisible({ + timeout: 15000, + }); + + // Wait for validation to complete - submit button should be enabled when done + // (memo already added, so no "Add Memo" button should appear) + await expect(page.getByTestId("SubmitAction")).toBeEnabled({ + timeout: 5000, + }); + + // Verify review modal opens and "Add Memo" button is not visible (memo already added) + await expect(page.getByTestId("AddMemoAction")).not.toBeVisible(); + await expect(page.getByTestId("review-tx-memo")).toHaveText("test memo"); +}); + +test("Send payment returns to review modal after adding memo from review flow", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await stubMemoRequiredAccounts(page, 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 page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); + await page.getByText("Continue").click(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Click Review Send to open review modal + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Wait for memo validation to complete - the "Add Memo" button appears when validation is done + await expect(page.getByTestId("AddMemoAction")).toBeVisible({ + timeout: 10000, + }); + + // Click Add Memo from review modal + await page.getByTestId("AddMemoAction").click(); + + // Fill and save memo + await expect(page.getByTestId("edit-memo-input")).toBeVisible(); + await page.getByTestId("edit-memo-input").fill("review memo"); + await page.getByText("Save").click(); + + // Verify review modal is reopened after saving memo + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + await expect(page.getByTestId("AddMemoAction")).not.toBeVisible(); + await expect(page.getByTestId("review-tx-memo")).toHaveText("review memo"); +}); + +test("Send payment returns to review modal after cancelling memo editor from review flow", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await stubAccountBalances(page); + await stubAccountHistory(page); + await stubTokenDetails(page); + await stubTokenPrices(page); + await stubMemoRequiredAccounts(page, 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 page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); + await page.getByText("Continue").click(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Click Review Send to open review modal + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Wait for memo validation to complete - the "Add Memo" button appears when validation is done + await expect(page.getByTestId("AddMemoAction")).toBeVisible({ + timeout: 10000, + }); + + // Click Add Memo from review modal + await page.getByTestId("AddMemoAction").click(); + + // Cancel memo editor + await expect(page.getByTestId("edit-memo-input")).toBeVisible(); + await page.getByText("Cancel").click(); + + // Verify review modal is reopened after cancelling and "Add Memo" button is still visible + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + await expect(page.getByTestId("AddMemoAction")).toBeVisible(); +}); + +test("Send payment shows memo value directly when memo is added before review", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await stubAccountBalances(page); + await stubAccountHistory(page); + await stubTokenDetails(page); + await stubTokenPrices(page); + await stubMemoRequiredAccounts(page, 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 page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); + await page.getByText("Continue").click(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Add memo before going to review + await page.getByTestId("send-amount-btn-memo").click(); + await page.getByTestId("edit-memo-input").fill("pre-review memo"); + await page.getByText("Save").click(); + + // After saving memo, a simulation is triggered to regenerate XDR with the new memo + // Wait for the memo editor to close + await expect(page.getByTestId("edit-memo-input")).not.toBeVisible({ + timeout: 5000, + }); + + // Wait for any loading overlays to disappear + await page.waitForTimeout(500); + + // Wait for the simulation to complete before clicking Review Send + // Verify we're still on the send amount page + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + // Wait for the simulation to complete before clicking Review Send + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 30000 }); + + // Click the button to start simulation and open review sheet + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open - this happens after simulation completes + // The simulation runs, then setIsReviewingTx(true) is called + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Wait for the review transaction content to be ready (not in loading state) + // Check that SubmitAction is visible (validation complete) + await expect(page.getByTestId("SubmitAction")).toBeVisible({ + timeout: 15000, + }); + + // Wait for validation to complete - submit button should be enabled when done + await expect(page.getByTestId("SubmitAction")).toBeEnabled({ + timeout: 5000, + }); + + // Verify review modal opens and shows memo directly, "Add Memo" button is not visible + await expect(page.getByTestId("AddMemoAction")).not.toBeVisible(); + await expect(page.getByTestId("review-tx-memo")).toHaveText( + "pre-review memo", + ); + // Verify the "Send to" button is visible (not "Add Memo") + await expect(page.getByTestId("SubmitAction")).toBeVisible(); +}); + +test("Send payment shows Add Memo when switching from non-memo-required to memo-required address", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + const NON_MEMO_REQUIRED_ADDRESS = + "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"; + + await stubAccountBalances(page); + await stubAccountHistory(page); + await stubTokenDetails(page); + await stubTokenPrices(page); + await stubMemoRequiredAccounts(page, 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(); + + // 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(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Go to review with non-memo-required address + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Wait for validation to complete - submit button should be enabled when done + await expect(page.getByTestId("SubmitAction")).toBeEnabled({ + timeout: 10000, + }); + + // Verify "Add Memo" button is not visible for non-memo-required address + await expect(page.getByTestId("AddMemoAction")).not.toBeVisible(); + + // Go back to change address + await page.getByText("Cancel").click(); + + // Change to memo-required address + 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(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + // Make sure amount is still 1 XLM after switching addresses + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Go to review again with memo-required address + await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Wait for memo validation to complete - the "Add Memo" button appears when validation is done + await expect(page.getByTestId("AddMemoAction")).toBeVisible({ + timeout: 10000, + }); +}); + +test("Send payment shows Add Memo after cancelling review and returning to memo-required address", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await stubMemoRequiredAccounts(page, 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 page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); + await page.getByText("Continue").click(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Go to review - should show "Add Memo" button + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Wait for memo validation to complete - the "Add Memo" button appears when validation is done + await expect(page.getByTestId("AddMemoAction")).toBeVisible({ + timeout: 10000, + }); + + // Cancel review + await page.getByText("Cancel").click(); + + // Wait for review sheet to close completely + await expect(page.getByText("You are sending")).not.toBeVisible({ + timeout: 5000, + }); + + // Verify we're back on the send amount page + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + // Ensure amount is still set after cancelling + await expect(page.getByTestId("send-amount-amount-input")).toHaveValue("1"); + + // Wait a moment for UI to stabilize after modal closes + await page.waitForTimeout(300); + + // Re-query the button after cancelling (button reference might be stale) + // Wait for button to be visible and enabled + await expect(page.getByTestId("send-amount-btn-continue")).toBeVisible({ + timeout: 5000, + }); + await expect(page.getByTestId("send-amount-btn-continue")).toBeEnabled({ + timeout: 10000, + }); + + // Click the button to open review again + await page.getByTestId("send-amount-btn-continue").click({ force: true }); + + // Wait for review sheet to open + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Wait for memo validation to complete - the "Add Memo" button appears when validation is done + await expect(page.getByTestId("AddMemoAction")).toBeVisible({ + timeout: 10000, + }); + await expect(page.getByText("Add Memo")).toBeVisible(); +}); + +// Classic token to G address -> Normal (regression test) +test("Send classic token to G address allows memo", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + await loginToTestAccount({ page, extensionId, context }); + await page.getByTestId("nav-link-send").click({ force: true }); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + 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(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Add memo - should be enabled for classic token to G address + await page.getByTestId("send-amount-btn-memo").click(); + await expect(page.getByTestId("edit-memo-input")).toBeVisible(); + await page.getByTestId("edit-memo-input").fill("classic G memo"); + await page.getByText("Save").click(); + + // Click Review Send + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Verify memo is shown in review + await expect(page.getByTestId("review-tx-memo")).toHaveText("classic G memo"); +}); + +// Classic token to M address -> Memo disabled (this is supported, but an antipattern) +test("Send classic token to M address doesn't allow memo", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + 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 page.getByTestId("send-to-input").fill(TEST_M_ADDRESS); + await page.getByText("Continue").click(); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1"); + + // Add memo - should be enabled for classic token to M address + await page.getByTestId("send-amount-btn-memo").click(); + await expect(page.getByTestId("edit-memo-input")).toBeVisible(); + // Memo input should be disabled + await expect(page.getByTestId("edit-memo-input")).toBeDisabled(); +}); + +// Custom token without Soroban mux support to G -> Memo NOT allowed +test("Send custom token without Soroban mux support to G address disables memo", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + const stubOverrides = async () => { + await stubAccountBalancesE2e(page); + }; + // Stub contract spec before login to ensure it's ready when needed + await stubContractSpec(page, TEST_TOKEN_ADDRESS, false); + + 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, + }); + + 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({ + timeout: 30000, + }); + + // Select custom token + await page.locator(".SendAmount__EditDestAsset").click(); + await page + .getByTestId(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) + .click({ force: true }); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible({ + timeout: 30000, + }); + await page.getByTestId("send-amount-amount-input").fill(".001"); + + // Wait for contract check to complete + // The memo button should be visible and enabled (user can click it) + const memoButton = page.getByTestId("send-amount-btn-memo"); + await expect(memoButton).toBeVisible({ timeout: 10000 }); + await expect(memoButton).toBeEnabled({ timeout: 10000 }); + + // Click memo button to open EditMemo + await memoButton.click(); + + // Verify EditMemo dialog is open and memo input field is visible + await expect(page.getByTestId("edit-memo-input")).toBeVisible({ + timeout: 10000, + }); + + // Verify memo input field is disabled (not editable) + await expect(page.getByTestId("edit-memo-input")).toBeDisabled(); + + // Verify the warning message is shown explaining why memo is disabled + await expect( + page.getByText("Memo is not supported for this operation"), + ).toBeVisible({ timeout: 10000 }); + + // Verify Save button is also disabled (since memo field is disabled) + const saveButton = page.getByRole("button", { name: /save/i }); + await expect(saveButton).toBeDisabled(); + + // Close the EditMemo dialog + await page.getByText("Cancel").click(); + + // Wait for EditMemo to close + await expect(page.getByTestId("edit-memo-input")).not.toBeVisible(); + + // Click Review Send - should be enabled (transaction is allowed, just no memo) + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 15000 }); + + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Verify memo row is NOT shown in review (tokens without Soroban mux support don't support memo) + // The memo row should be completely hidden, not showing "Memo None" + await expect(page.getByTestId("review-tx-memo")).not.toBeVisible(); +}); + +// Custom token without Soroban mux support to M -> Impossible/disabled +test("Send custom token without Soroban mux support to M address is disabled", async ({ + page, + extensionId, + context, +}) => { + test.slow(); + const stubOverrides = async () => { + await stubAccountBalancesE2e(page); + await stubSimulateTokenTransfer(page); + }; + // Stub contract spec before login to ensure it's ready when needed + await stubContractSpec(page, TEST_TOKEN_ADDRESS, false); + + 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 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(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) + .click({ force: true }); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill(".001"); + + // Wait for contract check to complete - wait for warning banner to appear + await expect( + page.getByText( + "This token does not support muxed address (M-) as a target destination", + ), + ).toBeVisible({ timeout: 10000 }); + + // Memo button should be enabled, but memo input should be disabled + const memoButton = page.getByTestId("send-amount-btn-memo"); + await expect(memoButton).toBeEnabled(); + + // Verify Review Send button is disabled + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeDisabled(); +}); + +// Custom token with Soroban mux support to G -> Memo can be added +test("Send custom token with Soroban mux support to G address allows memo", 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.getByTestId("nav-link-send").click({ force: true }); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + 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(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) + .click({ force: true }); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill(".001"); + + // Wait for contract check to complete - wait for memo button to be enabled + // (it might be disabled initially while contract check is in progress) + const memoButton = page.getByTestId("send-amount-btn-memo"); + await expect(memoButton).toBeEnabled({ timeout: 10000 }); + + // Add memo - should be enabled for custom token with Soroban mux support to G address + await memoButton.click(); + await expect(page.getByTestId("edit-memo-input")).toBeVisible({ + timeout: 10000, + }); + await expect(page.getByTestId("edit-memo-input")).toBeEnabled({ + timeout: 10000, + }); + await page.getByTestId("edit-memo-input").fill("soroban mux G memo"); + await page.getByText("Save").click(); + + // Click Review Send + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Verify memo is shown in review + await expect(page.getByTestId("review-tx-memo")).toHaveText( + "soroban mux G memo", + ); +}); + +// Custom token with Soroban mux support to M -> Memo disabled (embedded) +test("Send custom token with Soroban mux support to M address disables memo", 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.getByTestId("nav-link-send").click({ force: true }); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + + 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(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) + .click({ force: true }); + + await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill(".001"); + + // Wait for contract check to complete + // Since contract supports muxed (true), there should be NO warning banner + // (warning banner only shows when contractSupportsMuxed === false) + await page.waitForTimeout(2000); // Wait for contract check to complete + + // Verify NO warning banner is shown (contract supports muxed) + await expect( + page.getByText( + "This token does not support muxed address (M-) as a target destination", + ), + ).not.toBeVisible(); + + // Wait for the memo button to be enabled + const memoButton = page.getByTestId("send-amount-btn-memo"); + await expect(memoButton).toBeEnabled({ timeout: 10000 }); + + // Click memo button - memo should be disabled because destination is M address + // (memo is encoded in M address, so it's disabled) + await memoButton.click(); + await expect(page.getByTestId("edit-memo-input")).toBeVisible({ + timeout: 10000, + }); + await expect(page.getByTestId("edit-memo-input")).toBeDisabled(); + // Wait for the disabled message to appear + // The message should appear once the contract check completes + await expect( + page.getByText("Memo is disabled for this transaction"), + ).toBeVisible({ timeout: 15000 }); + await page.getByText("Cancel").click(); + + // Click Review Send + const reviewSendButton = page.getByTestId("send-amount-btn-continue"); + await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); + await reviewSendButton.click({ force: true }); + + // Wait for review sheet to open + await expect(page.getByText("You are sending")).toBeVisible({ + timeout: 200000, + }); + + // Verify memo row is NOT shown in review (memo is embedded in M address for tokens with Soroban mux support) + await expect(page.getByTestId("review-tx-memo")).not.toBeVisible(); +}); diff --git a/extension/e2e-tests/onboarding.test.ts b/extension/e2e-tests/onboarding.test.ts index 6dbd9de976..f72b50bd16 100644 --- a/extension/e2e-tests/onboarding.test.ts +++ b/extension/e2e-tests/onboarding.test.ts @@ -375,9 +375,13 @@ test("Incorrect mnemonic phrase", async ({ page }) => { ); }); -test("Logout and create new account", async ({ page, extensionId }) => { +test("Logout and create new account", async ({ + page, + extensionId, + context, +}) => { test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("account-view-account-name").click(); const originalAccounts = page.getByTestId("wallet-row-select"); @@ -442,9 +446,13 @@ test("Logout and create new account", async ({ page, extensionId }) => { }); }); -test("Logout and import new account", async ({ page, extensionId }) => { +test("Logout and import new account", async ({ + page, + extensionId, + context, +}) => { test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("account-view-account-name").click(); const originalAccounts = page.getByTestId("wallet-row-select"); @@ -735,17 +743,19 @@ test("Switch mnemonic phrase length preserves previous state (pw + ToS)", async await startImportWalletFlow({ page, password: PASSWORD_TEST }); const toggleLabel = page.locator('label[for="RecoverAccount__toggle"]'); - await toggleLabel.click(); + const input12 = page.locator('input[name="MnemonicPhrase-12"]'); + const input13 = page.locator('input[name="MnemonicPhrase-13"]'); - await page - .locator('input[name="MnemonicPhrase-13"]') - .waitFor({ state: "visible" }); + await expect(toggleLabel).toBeVisible(); + await expect(input12).toBeVisible(); + await expect(input13).toBeHidden(); await toggleLabel.click(); + await expect(input13).toBeVisible(); - await page - .locator('input[name="MnemonicPhrase-12"]') - .waitFor({ state: "visible" }); + await toggleLabel.click(); + await expect(input13).toBeHidden(); + await expect(input12).toBeVisible(); await fillMnemonicInputs({ page, words: TEST_MNEMONIC_12_WORDS }); diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/account-creator-overwrite-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/account-creator-overwrite-darwin.png new file mode 100644 index 0000000000..d8213f7048 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/account-creator-overwrite-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/confirm-recovery-page-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/confirm-recovery-page-darwin.png new file mode 100644 index 0000000000..69fb423330 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/confirm-recovery-page-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/incorrect-recovery-phrase-page-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/incorrect-recovery-phrase-page-darwin.png new file mode 100644 index 0000000000..e328401940 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/incorrect-recovery-phrase-page-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/recovery-bad-password-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/recovery-bad-password-darwin.png new file mode 100644 index 0000000000..02665c77b8 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/recovery-bad-password-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/recovery-modal-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/recovery-modal-darwin.png new file mode 100644 index 0000000000..6bab7a5541 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/recovery-modal-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/recovery-page-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/recovery-page-darwin.png new file mode 100644 index 0000000000..b16bbf2085 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/recovery-page-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/unlock-password-overwrite-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/unlock-password-overwrite-darwin.png new file mode 100644 index 0000000000..1b8aef10b7 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/unlock-password-overwrite-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-create-complete-page-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-create-complete-page-darwin.png new file mode 100644 index 0000000000..500855a9c5 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-create-complete-page-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-import-12-word-phrase-page-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-import-12-word-phrase-page-darwin.png new file mode 100644 index 0000000000..906a2b84f5 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-import-12-word-phrase-page-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-import-24-word-phrase-page-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-import-24-word-phrase-page-darwin.png new file mode 100644 index 0000000000..edb2f35559 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-import-24-word-phrase-page-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-import-complete-page-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-import-complete-page-darwin.png new file mode 100644 index 0000000000..500855a9c5 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/wallet-import-complete-page-darwin.png differ diff --git a/extension/e2e-tests/onboarding.test.ts-snapshots/welcome-page-darwin.png b/extension/e2e-tests/onboarding.test.ts-snapshots/welcome-page-darwin.png new file mode 100644 index 0000000000..0e0c9bd7f3 Binary files /dev/null and b/extension/e2e-tests/onboarding.test.ts-snapshots/welcome-page-darwin.png differ diff --git a/extension/e2e-tests/sendCollectible.test.ts b/extension/e2e-tests/sendCollectible.test.ts index 3e580e4d81..ebbb74fe79 100644 --- a/extension/e2e-tests/sendCollectible.test.ts +++ b/extension/e2e-tests/sendCollectible.test.ts @@ -1,39 +1,31 @@ import { loginToTestAccount } from "./helpers/login"; -import { TEST_M_ADDRESS } from "./helpers/test-token"; import { - stubAccountBalances, - stubAccountHistory, - stubCollectibles, - stubScanDapp, - stubScanTx, stubSimulateSendCollectible, - stubTokenDetails, - stubTokenPrices, - stubSubmitTx, stubCollectiblesUnsuccessfulMetadata, - stubFeeStats, stubContractSpec, } from "./helpers/stubs"; +import { TEST_M_ADDRESS } from "./helpers/test-token"; import { test, expect } from "./test-fixtures"; +// Review screen navigation and collectible details display issues test("Send collectible with metadata", async ({ page, extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await stubCollectibles(page); - await stubSimulateSendCollectible(page); - await stubScanTx(page); - await stubSubmitTx(page); - await stubFeeStats(page); - - await loginToTestAccount({ page, extensionId }); - await page.getByTestId("nav-link-send").click({ force: true }); + const stubOverrides = async () => { + await stubSimulateSendCollectible(page); + }; + + // Stub contract spec with muxed support = true + await stubContractSpec( + page, + "CCTYMI5ME6NFJC675P2CHNVG467YQJQ5E4TWP5RAPYYNKWK7DIUUDENN", + true, + ); + + 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( @@ -71,11 +63,10 @@ test("Send collectible with metadata", async ({ await page.getByText("Review Send").scrollIntoViewIfNeeded(); await page.getByText("Review Send").click({ force: true }); - // validate that fee is updated in the review screen to include minimum resource fee + // validate that fee is updated in the review screen await expect(page.getByTestId("send-amount-fee-display")).toHaveText( "0.00002 XLM", ); - await expect(page.getByText("You are sending")).toBeVisible(); await expect( page.getByTestId("review-tx-send-asset-collectible-name"), @@ -104,18 +95,19 @@ test("Send collectible without metadata", async ({ extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await stubCollectiblesUnsuccessfulMetadata(page); - await stubSimulateSendCollectible(page); - await stubScanTx(page); - await stubSubmitTx(page); - await stubFeeStats(page); - - await loginToTestAccount({ page, extensionId }); + const stubOverrides = async () => { + await stubCollectiblesUnsuccessfulMetadata(page); + await stubSimulateSendCollectible(page); + }; + + // Stub contract spec with muxed support = true + await stubContractSpec( + page, + "CCTYMI5ME6NFJC675P2CHNVG467YQJQ5E4TWP5RAPYYNKWK7DIUUDENN", + true, + ); + + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); @@ -151,13 +143,11 @@ test("Send collectible without metadata", async ({ await page.getByText("Review Send").scrollIntoViewIfNeeded(); await page.getByText("Review Send").click(); - // validate that fee is updated in the review screen to include minimum resource fee + // validate that fee is updated in the review screen await expect(page.getByTestId("send-amount-fee-display")).toHaveText( "0.00002 XLM", ); - await expect(page.getByText("You are sending")).toBeVisible(); - await expect( page.getByTestId("review-tx-send-asset-collectible-name"), ).toHaveText("Token #3"); @@ -186,14 +176,10 @@ test("Send collectible to M address when contract doesn't support muxed is disab extensionId, context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await stubCollectibles(page); - await stubSimulateSendCollectible(page); - await stubFeeStats(page); + const stubOverrides = async () => { + await stubSimulateSendCollectible(page); + }; + // Stub contract spec with muxed support = false await stubContractSpec( page, @@ -201,7 +187,7 @@ test("Send collectible to M address when contract doesn't support muxed is disab false, ); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); @@ -236,16 +222,9 @@ test("Send collectible with Soroban mux support to M address disables memo", asy context, }) => { test.slow(); - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await stubCollectibles(page); - await stubSimulateSendCollectible(page); - await stubScanTx(page); - await stubSubmitTx(page); - await stubFeeStats(page); + const stubOverrides = async () => { + await stubSimulateSendCollectible(page); + }; // Stub contract spec with muxed support = true await stubContractSpec( page, @@ -253,7 +232,7 @@ test("Send collectible with Soroban mux support to M address disables memo", asy true, ); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); @@ -309,17 +288,6 @@ test("Send collectible without Soroban mux support to G address disables memo", extensionId, context, }) => { - test.slow(); - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await stubCollectibles(page); - await stubSimulateSendCollectible(page); - await stubScanTx(page); - await stubSubmitTx(page); - await stubFeeStats(page); // Stub contract spec with muxed support = false await stubContractSpec( page, @@ -327,7 +295,7 @@ test("Send collectible without Soroban mux support to G address disables memo", false, ); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); await page.getByTestId("send-amount-edit-dest-asset").click(); @@ -376,17 +344,9 @@ test("Send collectible with Soroban mux support to G address allows memo", async extensionId, context, }) => { - test.slow(); - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - await stubScanDapp(context); - await stubCollectibles(page); - await stubSimulateSendCollectible(page); - await stubScanTx(page); - await stubSubmitTx(page); - await stubFeeStats(page); + const stubOverrides = async () => { + await stubSimulateSendCollectible(page); + }; // Stub contract spec with muxed support = true await stubContractSpec( page, @@ -394,8 +354,8 @@ test("Send collectible with Soroban mux support to G address allows memo", async true, ); - await loginToTestAccount({ page, extensionId }); - await page.getByTestId("nav-link-send").click({ force: true }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + await page.getByTestId("nav-link-send").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); diff --git a/extension/e2e-tests/sendPayment.test.ts b/extension/e2e-tests/sendPayment.test.ts index 9c05a6c66d..f81bf5f580 100644 --- a/extension/e2e-tests/sendPayment.test.ts +++ b/extension/e2e-tests/sendPayment.test.ts @@ -1,17 +1,10 @@ -import { TransactionBuilder } from "stellar-sdk"; import { test, expect } from "./test-fixtures"; -import { login, loginAndFund, loginToTestAccount } from "./helpers/login"; -import { TEST_M_ADDRESS, TEST_TOKEN_ADDRESS } from "./helpers/test-token"; +import { login, loginToTestAccount } from "./helpers/login"; +import { TEST_TOKEN_ADDRESS } from "./helpers/test-token"; import { - stubAccountBalances, stubAccountBalancesE2e, stubAccountBalancesWithUSDC, - stubAccountHistory, stubContractSpec, - stubMemoRequiredAccounts, - stubSimulateTokenTransfer, - stubTokenDetails, - stubTokenPrices, } from "./helpers/stubs"; const MUXED_ACCOUNT_ADDRESS = @@ -21,7 +14,6 @@ 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(); @@ -30,148 +22,149 @@ test("Swap doesn't throw error when account is unfunded", async ({ test("Swap shows correct balances for assets", async ({ page, extensionId, + context, }) => { - await stubAccountHistory(page); - await stubTokenDetails(page); - await page.route("*/**/account-balances/*", async (route) => { - const json = { - balances: { - "FOO:GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY": { - token: { - type: "credit_alphanum12", - code: "FOO", - issuer: { - key: "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - }, - }, - sellingLiabilities: "0", - buyingLiabilities: "0", - total: "100", - limit: "922337203685.4775807", - available: "100", - blockaidData: { - result_type: "Benign", - malicious_score: "0.0", - attack_types: {}, - chain: "stellar", - address: - "FOO-GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - metadata: { - external_links: {}, - }, - fees: {}, - features: [], - trading_limits: {}, - financial_stats: { - top_holders: [], + const stubOverrides = async () => { + await page.route("*/**/account-balances/*", async (route) => { + const json = { + balances: { + "FOO:GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY": { + token: { + type: "credit_alphanum12", + code: "FOO", + issuer: { + key: "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + }, }, - }, - }, - "BAZ:GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY": { - token: { - type: "credit_alphanum12", - code: "BAZ", - issuer: { - key: "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + sellingLiabilities: "0", + buyingLiabilities: "0", + total: "100", + limit: "922337203685.4775807", + available: "100", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: + "FOO-GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + metadata: { + external_links: {}, + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: { + top_holders: [], + }, }, }, - sellingLiabilities: "0", - buyingLiabilities: "0", - total: "10", - limit: "922337203685.4775807", - available: "10", - blockaidData: { - result_type: "Benign", - malicious_score: "0.0", - attack_types: {}, - chain: "stellar", - address: - "BAZ-GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", - metadata: { - external_links: {}, + "BAZ:GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY": { + token: { + type: "credit_alphanum12", + code: "BAZ", + issuer: { + key: "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + }, }, - fees: {}, - features: [ - { - feature_id: "HIGH_REPUTATION_TOKEN", - type: "Benign", - description: "Token with verified high reputation", + sellingLiabilities: "0", + buyingLiabilities: "0", + total: "10", + limit: "922337203685.4775807", + available: "10", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: + "BAZ-GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY", + metadata: { + external_links: {}, + }, + fees: {}, + features: [ + { + feature_id: "HIGH_REPUTATION_TOKEN", + type: "Benign", + description: "Token with verified high reputation", + }, + ], + trading_limits: {}, + financial_stats: { + top_holders: [], }, - ], - trading_limits: {}, - financial_stats: { - top_holders: [], }, }, - }, - native: { - token: { - type: "native", - code: "XLM", - }, - total: "999", - available: "999", - sellingLiabilities: "0", - buyingLiabilities: "0", - minimumBalance: "1", - blockaidData: { - result_type: "Benign", - malicious_score: "0.0", - attack_types: {}, - chain: "stellar", - address: "", - metadata: { - type: "", + native: { + token: { + type: "native", + code: "XLM", }, - fees: {}, - features: [], - trading_limits: {}, - financial_stats: {}, - }, - }, - "PBT:GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY": { - token: { - code: "PBT", - issuer: { - key: "CAZXRTOKNUQ2JQQF3NCRU7GYMDJNZ2NMQN6IGN4FCT5DWPODMPVEXSND", + total: "999", + available: "999", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "1", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, }, }, - contractId: - "CAZXRTOKNUQ2JQQF3NCRU7GYMDJNZ2NMQN6IGN4FCT5DWPODMPVEXSND", - symbol: "PBT", - decimals: 5, - total: "9899700", - available: "9899700", - blockaidData: { - result_type: "Benign", - malicious_score: "0.0", - attack_types: {}, - chain: "stellar", - address: - "PBT-CAZXRTOKNUQ2JQQF3NCRU7GYMDJNZ2NMQN6IGN4FCT5DWPODMPVEXSND", - metadata: { - external_links: {}, + "PBT:GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY": { + token: { + code: "PBT", + issuer: { + key: "CAZXRTOKNUQ2JQQF3NCRU7GYMDJNZ2NMQN6IGN4FCT5DWPODMPVEXSND", + }, }, - fees: {}, - features: [], - trading_limits: {}, - financial_stats: { - top_holders: [], + contractId: + "CAZXRTOKNUQ2JQQF3NCRU7GYMDJNZ2NMQN6IGN4FCT5DWPODMPVEXSND", + symbol: "PBT", + decimals: 5, + total: "9899700", + available: "9899700", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: + "PBT-CAZXRTOKNUQ2JQQF3NCRU7GYMDJNZ2NMQN6IGN4FCT5DWPODMPVEXSND", + metadata: { + external_links: {}, + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: { + top_holders: [], + }, }, }, }, - }, - isFunded: true, - subentryCount: 0, - error: { - horizon: null, - soroban: null, - }, - }; - await route.fulfill({ json }); - }); + isFunded: true, + subentryCount: 0, + error: { + horizon: null, + soroban: null, + }, + }; + await route.fulfill({ json }); + }); + }; test.slow(); - await login({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-swap").click(); await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); @@ -189,9 +182,10 @@ test("Swap shows correct balances for assets", async ({ test("Send doesn't throw error when account is unfunded", async ({ page, extensionId, + context, }) => { test.slow(); - await login({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); @@ -205,35 +199,62 @@ test("Send doesn't throw error when account is unfunded", async ({ await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); }); + test("Send doesn't throw error when creating muxed account", async ({ page, extensionId, + context, }) => { test.slow(); - await loginAndFund({ page, extensionId }); - 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 page.getByTestId("send-to-input").fill(MUXED_ACCOUNT_ADDRESS); - await expect( - page.getByText("The destination account doesn’t exist."), - ).toBeVisible(); - await page.getByText("Continue").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1"); - await page.getByText("Review Send").click({ force: true }); - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); -}); + const stubOverrides = async () => { + // Override account-balances to return 0 XLM only for the muxed account address + await page.route("**/account-balances/**", async (route) => { + const url = route.request().url(); + const isMuxedAccount = url.includes( + "GCQ7EGW7VXHI4AKJAFADOIHCSK2OCVA42KUETUK5LQ3LVSEQEEKP7O7B", + ); + + const json = { + balances: { + native: { + token: { + type: "native", + code: "XLM", + }, + total: isMuxedAccount ? "0" : "10000.0000000", + available: isMuxedAccount ? "0" : "10000.0000000", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "1", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, + }, + }, + }, + isFunded: !isMuxedAccount, + subentryCount: 0, + error: { + horizon: null, + soroban: null, + }, + }; + await route.fulfill({ json }); + }); + }; -test("Send can review formatted inputs", async ({ page, extensionId }) => { - test.slow(); - await loginAndFund({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); @@ -246,278 +267,100 @@ test("Send can review formatted inputs", async ({ page, extensionId }) => { ).toBeVisible(); await page.getByText("Continue").click(); - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1000"); - await page.getByText("Review Send").click({ force: true }); - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); -}); - -test("Send persists inputs and submits to network", async ({ - page, - extensionId, -}) => { - test.slow(); - let isScanSkiped = false; - page.on("request", (request) => { - if ( - request - .url() - .includes("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF") - ) { - isScanSkiped = request.url().includes("should_skip_scan=true"); - } - }); - - await loginAndFund({ page, extensionId }); - 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 page - .getByTestId("send-to-input") - .fill("GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"); - - await page.getByText("Continue").click(); - expect(isScanSkiped).toBeTruthy(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); await page.getByTestId("send-amount-amount-input").fill("1"); - // Add memo - await page.getByTestId("send-amount-btn-memo").click(); - await page.getByTestId("edit-memo-input").fill("test memo"); - await page.getByText("Save").click(); - - // Wait for memo editor to close - await expect(page.getByTestId("edit-memo-input")).not.toBeVisible({ - timeout: 5000, - }); - - // Add fee - await page.getByTestId("send-amount-btn-fee").click(); - await page.getByTestId("edit-tx-settings-fee-input").fill("0.00009"); - await page.getByText("Save").click(); - - // Wait for fee editor to close - await expect(page.getByTestId("edit-tx-settings-fee-input")).not.toBeVisible({ - timeout: 5000, - }); - - // Wait for simulation to complete - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 30000 }); - - // Click Review Send button + const reviewSendButton = page.getByText("Review Send"); + await expect(reviewSendButton).toBeEnabled({ timeout: 15000 }); await reviewSendButton.click({ force: true }); - // Verify review modal opens await expect(page.getByText("You are sending")).toBeVisible({ timeout: 200000, }); - await expect(page.getByTestId("review-tx-send-amount")).toHaveText("1 XLM"); - await expect(page.getByTestId("review-tx-memo")).toHaveText("test memo"); - await expect(page.getByTestId("review-tx-fee")).toHaveText("0.00009 XLM"); - - let submitTxResponse = ""; - page.on("response", async (response) => { - if (response.url().includes("/submit-tx")) { - submitTxResponse = await response.text(); - } - }); - - await page.getByTestId(`SubmitAction`).click(); - - await expect(page.getByText("Sent!")).toBeVisible({ - timeout: 60000, - }); - const submitTxResponseJson = JSON.parse(submitTxResponse); - - expect(submitTxResponseJson.memo).toBe("test memo"); - expect(submitTxResponseJson.max_fee).toBe("900"); - - const tx = TransactionBuilder.fromXDR( - submitTxResponseJson.envelope_xdr, - "Test SDF Network ; September 2015", - ); - - const txOp = tx.operations[0] as any; - - expect(txOp.type).toBe("payment"); - expect(txOp.amount).toBe("1.0000000"); - expect(txOp.destination).toBe( - "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF", - ); - expect(txOp.asset.code).toBe("XLM"); - - await page.getByText("Done").click(); }); -test("Send XLM payments to recent federated addresses", async ({ +test("Send can review formatted inputs", async ({ page, extensionId, + context, }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - test.slow(); - await loginToTestAccount({ page, extensionId }); - 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 page.getByTestId("send-to-input").fill("freighter.pb*lobstr.co"); - await expect(page.getByTestId("send-to-identicon")).toBeVisible(); - await page.getByText("Continue").click({ force: true }); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1"); - await page.getByText("Review Send").click({ force: true }); - - await expect(page.getByText("You are sending")).toBeVisible(); - const submitAction = page.getByTestId("SubmitAction"); - - await page.waitForTimeout(300); - await submitAction.waitFor({ state: "visible" }); - await submitAction.click({ force: true }); - - await expect(page.getByText("Sent!")).toBeVisible({ - timeout: 60000, - }); - - 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 page.getByTestId("address-tile").click(); - - await expect(page.getByText("Recents")).toBeVisible(); - - await page.getByTestId("recent-address-button").click(); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1"); - await page.getByText("Review Send").click({ force: true }); - - await expect(page.getByText("You are sending")).toBeVisible(); - - await page.waitForTimeout(300); - await submitAction.waitFor({ state: "visible" }); - await submitAction.click({ force: true }); - - let accountBalancesRequestWasMade = false; - page.on("request", (request) => { - if (request.url().includes("/account-balances/")) { - accountBalancesRequestWasMade = true; - } - }); - - await expect(page.getByText("Sent!")).toBeVisible({ - timeout: 60000, - }); - expect(accountBalancesRequestWasMade).toBeTruthy(); -}); -test.skip("Send XLM payment to C address", async ({ page, extensionId }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - - test.slow(); - await loginToTestAccount({ page, extensionId }); + const stubOverrides = async () => { + // Override account-balances to return 0 XLM only for the muxed account address + await page.route("**/account-balances/**", async (route) => { + const url = route.request().url(); + const isMuxedAccount = url.includes( + "GCQ7EGW7VXHI4AKJAFADOIHCSK2OCVA42KUETUK5LQ3LVSEQEEKP7O7B", + ); + + const json = { + balances: { + native: { + token: { + type: "native", + code: "XLM", + }, + total: isMuxedAccount ? "0" : "10000.0000000", + available: isMuxedAccount ? "0" : "10000.0000000", + sellingLiabilities: "0", + buyingLiabilities: "0", + minimumBalance: "1", + blockaidData: { + result_type: "Benign", + malicious_score: "0.0", + attack_types: {}, + chain: "stellar", + address: "", + metadata: { + type: "", + }, + fees: {}, + features: [], + trading_limits: {}, + financial_stats: {}, + }, + }, + }, + isFunded: !isMuxedAccount, + subentryCount: 0, + error: { + horizon: null, + soroban: null, + }, + }; + await route.fulfill({ json }); + }); + }; + 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 page.getByTestId("address-tile").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.getByTestId("send-amount-amount-input").fill(".001"); - await page.getByText("Review Send").click({ force: true }); - - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 60000, - }); - - const submitAction = page.getByTestId("SubmitAction"); - - await page.waitForTimeout(300); - await submitAction.waitFor({ state: "visible" }); - await submitAction.click({ force: true, timeout: 60000 }); - - let accountBalancesRequestWasMade = false; - page.on("request", (request) => { - if (request.url().includes("/account-balances/")) { - accountBalancesRequestWasMade = true; - } - }); - - await expect(page.getByText("Sent!")).toBeVisible({ - timeout: 60000, - }); - expect(accountBalancesRequestWasMade).toBeTruthy(); -}); - -test("Send XLM payment to M address", async ({ page, extensionId }) => { - await stubTokenDetails(page); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - - test.slow(); - await loginToTestAccount({ page, extensionId }); - - await page.getByTestId("nav-link-send").click({ force: true }); + await page.getByTestId("send-to-input").fill(MUXED_ACCOUNT_ADDRESS); + await expect( + page.getByText("The destination account doesn’t exist."), + ).toBeVisible(); + await page.getByText("Continue").click(); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); + await page.getByTestId("send-amount-amount-input").fill("1000"); - 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("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill(".001"); - await page.getByText("Review Send").click(); + const reviewSendButton = page.getByText("Review Send"); + await expect(reviewSendButton).toBeEnabled({ timeout: 15000 }); + await reviewSendButton.click({ force: true }); await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 60000, - }); - - const submitButton = page.getByTestId("SubmitAction"); - await submitButton.scrollIntoViewIfNeeded(); - await submitButton.click(); - - let accountBalancesRequestWasMade = false; - page.on("request", (request) => { - if (request.url().includes("/account-balances/")) { - accountBalancesRequestWasMade = true; - } - }); - - await expect(page.getByText("Sent!")).toBeVisible({ - timeout: 60000, + timeout: 200000, }); - - expect(accountBalancesRequestWasMade).toBeTruthy(); }); -test.skip("Send SAC to C address", async ({ page, extensionId }) => { +test.fixme("Send SAC to C address", async ({ page, extensionId, context }) => { test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); // add USDC asset await page.getByTestId("account-options-dropdown").click(); @@ -614,67 +457,13 @@ test.skip("Send SAC to C address", async ({ page, extensionId }) => { }); }); -test.skip("Send token payment to C address", async ({ page, extensionId }) => { - await stubTokenDetails(page); - await stubAccountBalancesE2e(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - - test.slow(); - await loginToTestAccount({ page, extensionId }); - - 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 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.getByText("E2E").click({ force: true }); - - 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 }); - - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 60000, - }); - - await expect(page.getByTestId("SubmitAction")).toBeVisible({ - timeout: 60000, - }); - await page.getByTestId(`SubmitAction`).click({ force: true, timeout: 60000 }); - - let accountBalancesRequestWasMade = false; - page.on("request", (request) => { - if (request.url().includes("/account-balances/")) { - accountBalancesRequestWasMade = true; - } - }); - - await expect(page.getByText("Sent!")).toBeVisible({ - timeout: 60000, - }); - - expect(accountBalancesRequestWasMade).toBeTruthy(); -}); - test("SendPayment persists amount and asset when navigating to choose address", async ({ page, extensionId, + context, }) => { - await stubTokenDetails(page); - await stubAccountBalancesWithUSDC(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-send").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); @@ -693,14 +482,13 @@ test("SendPayment persists amount and asset when navigating to choose address", test("SendPayment resets amount when user selects new asset", async ({ page, extensionId, + context, }) => { - await stubTokenDetails(page); - await stubAccountBalancesWithUSDC(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - test.slow(); - await loginToTestAccount({ page, extensionId }); + const stubOverrides = async () => { + await stubAccountBalancesWithUSDC(page); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); @@ -718,14 +506,13 @@ test("SendPayment resets amount when user selects new asset", async ({ test("SendPayment resets state when navigating back to account", async ({ page, extensionId, + context, }) => { - await stubTokenDetails(page); - await stubAccountBalancesWithUSDC(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - test.slow(); - await loginToTestAccount({ page, extensionId }); + const stubOverrides = async () => { + await stubAccountBalancesWithUSDC(page); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-send").click({ force: true }); await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); @@ -748,19 +535,17 @@ 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 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"); }); test("Swap persists amount when navigating to choose source asset", async ({ page, extensionId, + context, }) => { - await stubTokenDetails(page); - await stubAccountBalancesWithUSDC(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("nav-link-swap").click(); await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); @@ -781,14 +566,13 @@ test("Swap persists amount when navigating to choose source asset", async ({ test("Swap resets amount when user selects new source asset", async ({ page, extensionId, + context, }) => { - await stubTokenDetails(page); - await stubAccountBalancesWithUSDC(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - test.slow(); - await loginToTestAccount({ page, extensionId }); + const stubOverrides = async () => { + await stubAccountBalancesWithUSDC(page); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-swap").click(); await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); @@ -807,14 +591,13 @@ test("Swap resets amount when user selects new source asset", async ({ test("Swap preserves amount when selecting destination asset", async ({ page, extensionId, + context, }) => { - await stubTokenDetails(page); - await stubAccountBalancesWithUSDC(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - test.slow(); - await loginToTestAccount({ page, extensionId }); + const stubOverrides = async () => { + await stubAccountBalancesWithUSDC(page); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-swap").click(); await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); @@ -833,14 +616,13 @@ test("Swap preserves amount when selecting destination asset", async ({ test("Swap resets state when navigating back to account", async ({ page, extensionId, + context, }) => { - await stubTokenDetails(page); - await stubAccountBalancesWithUSDC(page); - await stubAccountHistory(page); - await stubTokenPrices(page); - test.slow(); - await loginToTestAccount({ page, extensionId }); + const stubOverrides = async () => { + await stubAccountBalancesWithUSDC(page); + }; + await loginToTestAccount({ page, extensionId, context, stubOverrides }); await page.getByTestId("nav-link-swap").click(); await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); @@ -864,16 +646,18 @@ test("Swap resets state when navigating back to account", async ({ const newAmountInput = page.locator('input[type="text"]').first(); await expect(newAmountInput).toHaveValue("0"); + // Verify XLM is selected (more reliable than checking for "0 XLM" text) + await expect(page.getByTestId("swap-src-asset-tile")).toContainText("XLM"); }); -test.afterAll(async ({ page, extensionId }) => { +test.afterAll(async ({ page, extensionId, context }) => { if ( test.info().status !== test.info().expectedStatus && test.info().title === "Send SAC to C address" ) { // remove trustline in cleanup if Send SAC to C address test failed test.slow(); - await loginToTestAccount({ page, extensionId }); + await loginToTestAccount({ page, extensionId, context }); await page.getByTestId("account-options-dropdown").click(); await page.getByText("Manage assets").click({ force: true }); @@ -886,835 +670,70 @@ test.afterAll(async ({ page, extensionId }) => { } }); -const MEMO_REQUIRED_ADDRESS = - "GA6SXIZIKLJHCZI2KEOBEUUOFMM4JUPPM2UTWX6STAWT25JWIEUFIMFF"; - -// Reset environment variables before each memo-related test -// This ensures IS_PLAYWRIGHT is set for memo validation bypass -test.beforeEach(async ({ page }) => { - await page.evaluate(() => { - // Ensure IS_PLAYWRIGHT is set for memo validation bypass - (window as any).IS_PLAYWRIGHT = "true"; - }); -}); - -test("Send payment shows memo required warning when destination requires memo", async ({ +test("Send token payment from Asset Detail", async ({ page, extensionId, + context, }) => { test.slow(); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubMemoRequiredAccounts(page, MEMO_REQUIRED_ADDRESS); + const stubOverrides = async () => { + await stubAccountBalancesE2e(page); + }; + await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); - await loginToTestAccount({ page, extensionId }); - await page.getByTestId("nav-link-send").click({ force: true }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + 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(); - await page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); + 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("1"); + 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", + ); - // Click Review Send to trigger memo validation const reviewSendButton = page.getByTestId("send-amount-btn-continue"); await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); await reviewSendButton.click({ force: true }); - // Wait for review sheet to open - this happens after simulation completes - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); + await expect(page.getByText("You are sending")).toBeVisible(); - // Wait for the review transaction content to be ready (not in loading state) - // Check that AddMemoAction is visible (validation complete) - await expect(page.getByTestId("AddMemoAction")).toBeVisible({ + await expect(page.getByTestId("SubmitAction")).toBeVisible({ timeout: 15000, }); - await expect(page.getByText("Add Memo")).toBeVisible(); + await expect(page.getByTestId("SubmitAction")).toBeEnabled(); }); -test("Send payment allows submission after adding memo to memo-required address", async ({ +test("Send XLM payment from Asset Detail", async ({ page, extensionId, + context, }) => { test.slow(); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubMemoRequiredAccounts(page, MEMO_REQUIRED_ADDRESS); + const stubOverrides = async () => { + await stubAccountBalancesE2e(page); + }; + await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); - await loginToTestAccount({ page, extensionId }); - await page.getByTestId("nav-link-send").click({ force: true }); + await loginToTestAccount({ page, extensionId, context, stubOverrides }); + 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("address-tile").click(); - await page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); - await page.getByText("Continue").click(); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1"); - - // Add memo from SendPayment page - await page.getByTestId("send-amount-btn-memo").click(); - await page.getByTestId("edit-memo-input").fill("test memo"); - await page.getByText("Save").click(); - - // Verify memo was saved and review modal doesn't auto-open - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - expect(await page.getByText("You are sending").isVisible()).toBeFalsy(); - - // Click Review Send - this triggers simulation and opens review sheet - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - - // Wait for button to be enabled (simulation not in progress) before clicking - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); - - // Click the button to start simulation and open review sheet - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - this happens after simulation completes - // The simulation may take time, so we wait for the review sheet content - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Wait for the review transaction content to be ready (not in loading state) - // Check that either SubmitAction or AddMemoAction is visible (validation complete) - await expect( - page - .getByTestId("SubmitAction") - .or(page.getByTestId("AddMemoAction")) - .first(), - ).toBeVisible({ - timeout: 15000, - }); - - // Wait for validation to complete - submit button should be enabled when done - // (memo already added, so no "Add Memo" button should appear) - await expect(page.getByTestId("SubmitAction")).toBeEnabled({ - timeout: 5000, - }); - - // Verify review modal opens and "Add Memo" button is not visible (memo already added) - await expect(page.getByTestId("AddMemoAction")).not.toBeVisible(); - await expect(page.getByTestId("review-tx-memo")).toHaveText("test memo"); -}); - -test("Send payment returns to review modal after adding memo from review flow", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubMemoRequiredAccounts(page, MEMO_REQUIRED_ADDRESS); - - await loginToTestAccount({ page, extensionId }); - 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 page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); - await page.getByText("Continue").click(); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1"); - - // Click Review Send to open review modal - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Wait for memo validation to complete - the "Add Memo" button appears when validation is done - await expect(page.getByTestId("AddMemoAction")).toBeVisible({ - timeout: 10000, - }); - - // Click Add Memo from review modal - await page.getByTestId("AddMemoAction").click(); - - // Fill and save memo - await expect(page.getByTestId("edit-memo-input")).toBeVisible(); - await page.getByTestId("edit-memo-input").fill("review memo"); - await page.getByText("Save").click(); - - // Verify review modal is reopened after saving memo - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - await expect(page.getByTestId("AddMemoAction")).not.toBeVisible(); - await expect(page.getByTestId("review-tx-memo")).toHaveText("review memo"); -}); - -test("Send payment returns to review modal after cancelling memo editor from review flow", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubMemoRequiredAccounts(page, MEMO_REQUIRED_ADDRESS); - - await loginToTestAccount({ page, extensionId }); - 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 page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); - await page.getByText("Continue").click(); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1"); - - // Click Review Send to open review modal - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Wait for memo validation to complete - the "Add Memo" button appears when validation is done - await expect(page.getByTestId("AddMemoAction")).toBeVisible({ - timeout: 10000, - }); - - // Click Add Memo from review modal - await page.getByTestId("AddMemoAction").click(); - - // Cancel memo editor - await expect(page.getByTestId("edit-memo-input")).toBeVisible(); - await page.getByText("Cancel").click(); - - // Verify review modal is reopened after cancelling and "Add Memo" button is still visible - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - await expect(page.getByTestId("AddMemoAction")).toBeVisible(); -}); - -test("Send payment shows memo value directly when memo is added before review", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubMemoRequiredAccounts(page, MEMO_REQUIRED_ADDRESS); - - await loginToTestAccount({ page, extensionId }); - 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 page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); - await page.getByText("Continue").click(); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1"); - - // Add memo before going to review - await page.getByTestId("send-amount-btn-memo").click(); - await page.getByTestId("edit-memo-input").fill("pre-review memo"); - await page.getByText("Save").click(); - - // After saving memo, a simulation is triggered to regenerate XDR with the new memo - // Wait for the memo editor to close - await expect(page.getByTestId("edit-memo-input")).not.toBeVisible({ - timeout: 5000, - }); - - // Wait for any loading overlays to disappear - await page.waitForTimeout(500); - - // Wait for the simulation to complete before clicking Review Send - // Verify we're still on the send amount page - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - // Wait for the simulation to complete before clicking Review Send - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 30000 }); - - // Click the button to start simulation and open review sheet - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - this happens after simulation completes - // The simulation runs, then setIsReviewingTx(true) is called - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Wait for the review transaction content to be ready (not in loading state) - // Check that SubmitAction is visible (validation complete) - await expect(page.getByTestId("SubmitAction")).toBeVisible({ - timeout: 15000, - }); - - // Wait for validation to complete - submit button should be enabled when done - await expect(page.getByTestId("SubmitAction")).toBeEnabled({ - timeout: 5000, - }); - - // Verify review modal opens and shows memo directly, "Add Memo" button is not visible - await expect(page.getByTestId("AddMemoAction")).not.toBeVisible(); - await expect(page.getByTestId("review-tx-memo")).toHaveText( - "pre-review memo", - ); - // Verify the "Send to" button is visible (not "Add Memo") - await expect(page.getByTestId("SubmitAction")).toBeVisible(); -}); - -test("Send payment shows Add Memo when switching from non-memo-required to memo-required address", async ({ - page, - extensionId, -}) => { - test.slow(); - const NON_MEMO_REQUIRED_ADDRESS = - "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF"; - - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubMemoRequiredAccounts(page, MEMO_REQUIRED_ADDRESS); - - await loginToTestAccount({ page, extensionId }); - await page.getByTestId("nav-link-send").click({ force: true }); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - // 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(); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1"); - - // Go to review with non-memo-required address - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Wait for validation to complete - submit button should be enabled when done - await expect(page.getByTestId("SubmitAction")).toBeEnabled({ - timeout: 10000, - }); - - // Verify "Add Memo" button is not visible for non-memo-required address - await expect(page.getByTestId("AddMemoAction")).not.toBeVisible(); - - // Go back to change address - await page.getByText("Cancel").click(); - - // Change to memo-required address - 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(); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - // Make sure amount is still 1 XLM after switching addresses - await page.getByTestId("send-amount-amount-input").fill("1"); - - // Go to review again with memo-required address - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Wait for memo validation to complete - the "Add Memo" button appears when validation is done - await expect(page.getByTestId("AddMemoAction")).toBeVisible({ - timeout: 10000, - }); -}); - -test("Send payment shows Add Memo after cancelling review and returning to memo-required address", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubMemoRequiredAccounts(page, MEMO_REQUIRED_ADDRESS); - - await loginToTestAccount({ page, extensionId }); - 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 page.getByTestId("send-to-input").fill(MEMO_REQUIRED_ADDRESS); - await page.getByText("Continue").click(); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1"); - - // Go to review - should show "Add Memo" button - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Wait for memo validation to complete - the "Add Memo" button appears when validation is done - await expect(page.getByTestId("AddMemoAction")).toBeVisible({ - timeout: 10000, - }); - - // Cancel review - await page.getByText("Cancel").click(); - - // Wait for review sheet to close completely - await expect(page.getByText("You are sending")).not.toBeVisible({ - timeout: 5000, - }); - - // Verify we're back on the send amount page - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - // Ensure amount is still set after cancelling - await expect(page.getByTestId("send-amount-amount-input")).toHaveValue("1"); - - // Wait a moment for UI to stabilize after modal closes - await page.waitForTimeout(300); - - // Re-query the button after cancelling (button reference might be stale) - // Wait for button to be visible and enabled - await expect(page.getByTestId("send-amount-btn-continue")).toBeVisible({ - timeout: 5000, - }); - await expect(page.getByTestId("send-amount-btn-continue")).toBeEnabled({ - timeout: 10000, - }); - - // Click the button to open review again - await page.getByTestId("send-amount-btn-continue").click({ force: true }); - - // Wait for review sheet to open - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Wait for memo validation to complete - the "Add Memo" button appears when validation is done - await expect(page.getByTestId("AddMemoAction")).toBeVisible({ - timeout: 10000, - }); - await expect(page.getByText("Add Memo")).toBeVisible(); -}); - -// Classic token to G address -> Normal (regression test) -test("Send classic token to G address allows memo", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - - await loginToTestAccount({ page, extensionId }); - await page.getByTestId("nav-link-send").click({ force: true }); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - 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(); - await page.getByTestId("send-amount-amount-input").fill("1"); - - // Add memo - should be enabled for classic token to G address - await page.getByTestId("send-amount-btn-memo").click(); - await expect(page.getByTestId("edit-memo-input")).toBeVisible(); - await page.getByTestId("edit-memo-input").fill("classic G memo"); - await page.getByText("Save").click(); - - // Click Review Send - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Verify memo is shown in review - await expect(page.getByTestId("review-tx-memo")).toHaveText("classic G memo"); -}); - -// Classic token to M address -> Memo disabled (this is supported, but an antipattern) -test("Send classic token to M address doesn't allow memo", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubAccountBalances(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - - await loginToTestAccount({ page, extensionId }); - 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 page.getByTestId("send-to-input").fill(TEST_M_ADDRESS); - await page.getByText("Continue").click(); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill("1"); - - // Add memo - should be enabled for classic token to M address - await page.getByTestId("send-amount-btn-memo").click(); - await expect(page.getByTestId("edit-memo-input")).toBeVisible(); - // Memo input should be disabled - await expect(page.getByTestId("edit-memo-input")).toBeDisabled(); -}); - -// Custom token without Soroban mux support to G -> Memo NOT allowed -test("Send custom token without Soroban mux support to G address disables memo", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubAccountBalancesE2e(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubSimulateTokenTransfer(page); - // Stub contract spec before login to ensure it's ready when needed - await stubContractSpec(page, TEST_TOKEN_ADDRESS, false); - - await loginToTestAccount({ page, extensionId }); - await page.getByTestId("nav-link-send").click({ force: true }); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible({ - timeout: 30000, - }); - - 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({ - timeout: 30000, - }); - - // Select custom token - await page.locator(".SendAmount__EditDestAsset").click(); - await page - .getByTestId(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) - .click({ force: true }); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible({ - timeout: 30000, - }); - await page.getByTestId("send-amount-amount-input").fill(".001"); - - // Wait for contract check to complete - // The memo button should be visible and enabled (user can click it) - const memoButton = page.getByTestId("send-amount-btn-memo"); - await expect(memoButton).toBeVisible({ timeout: 10000 }); - await expect(memoButton).toBeEnabled({ timeout: 10000 }); - - // Click memo button to open EditMemo - await memoButton.click(); - - // Verify EditMemo dialog is open and memo input field is visible - await expect(page.getByTestId("edit-memo-input")).toBeVisible({ - timeout: 10000, - }); - - // Verify memo input field is disabled (not editable) - await expect(page.getByTestId("edit-memo-input")).toBeDisabled(); - - // Verify the warning message is shown explaining why memo is disabled - await expect( - page.getByText("Memo is not supported for this operation"), - ).toBeVisible({ timeout: 10000 }); - - // Verify Save button is also disabled (since memo field is disabled) - const saveButton = page.getByRole("button", { name: /save/i }); - await expect(saveButton).toBeDisabled(); - - // Close the EditMemo dialog - await page.getByText("Cancel").click(); - - // Wait for EditMemo to close - await expect(page.getByTestId("edit-memo-input")).not.toBeVisible(); - - // Click Review Send - should be enabled (transaction is allowed, just no memo) - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 15000 }); - - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Verify memo row is NOT shown in review (tokens without Soroban mux support don't support memo) - // The memo row should be completely hidden, not showing "Memo None" - await expect(page.getByTestId("review-tx-memo")).not.toBeVisible(); -}); - -// Custom token without Soroban mux support to M -> Impossible/disabled -test("Send custom token without Soroban mux support to M address is disabled", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubAccountBalancesE2e(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubSimulateTokenTransfer(page); - // Stub contract spec before login to ensure it's ready when needed - await stubContractSpec(page, TEST_TOKEN_ADDRESS, false); - - await loginToTestAccount({ page, extensionId }); - 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 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(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) - .click({ force: true }); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill(".001"); - - // Wait for contract check to complete - wait for warning banner to appear - await expect( - page.getByText( - "This token does not support muxed address (M-) as a target destination", - ), - ).toBeVisible({ timeout: 10000 }); - - // Memo button should be enabled, but memo input should be disabled - const memoButton = page.getByTestId("send-amount-btn-memo"); - await expect(memoButton).toBeEnabled(); - - // Verify Review Send button is disabled - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeDisabled(); -}); - -// Custom token with Soroban mux support to G -> Memo can be added -test("Send custom token with Soroban mux support to G address allows memo", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubAccountBalancesE2e(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubSimulateTokenTransfer(page); - await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); - - await loginToTestAccount({ page, extensionId }); - await page.getByTestId("nav-link-send").click({ force: true }); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - - 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(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) - .click({ force: true }); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill(".001"); - - // Wait for contract check to complete - wait for memo button to be enabled - // (it might be disabled initially while contract check is in progress) - const memoButton = page.getByTestId("send-amount-btn-memo"); - await expect(memoButton).toBeEnabled({ timeout: 10000 }); - - // Add memo - should be enabled for custom token with Soroban mux support to G address - await memoButton.click(); - await expect(page.getByTestId("edit-memo-input")).toBeVisible({ - timeout: 10000, - }); - await expect(page.getByTestId("edit-memo-input")).toBeEnabled({ - timeout: 10000, - }); - await page.getByTestId("edit-memo-input").fill("soroban mux G memo"); - await page.getByText("Save").click(); - - // Click Review Send - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Verify memo is shown in review - await expect(page.getByTestId("review-tx-memo")).toHaveText( - "soroban mux G memo", - ); -}); - -// Custom token with Soroban mux support to M -> Memo disabled (embedded) -test("Send custom token with Soroban mux support to M address disables memo", async ({ - page, - extensionId, -}) => { - test.slow(); - await stubAccountBalancesE2e(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubSimulateTokenTransfer(page); - await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); - - await loginToTestAccount({ page, extensionId }); - 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 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(`SendRow-E2E:${TEST_TOKEN_ADDRESS}`) - .click({ force: true }); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await page.getByTestId("send-amount-amount-input").fill(".001"); - - // Wait for contract check to complete - // Since contract supports muxed (true), there should be NO warning banner - // (warning banner only shows when contractSupportsMuxed === false) - await page.waitForTimeout(2000); // Wait for contract check to complete - - // Verify NO warning banner is shown (contract supports muxed) - await expect( - page.getByText( - "This token does not support muxed address (M-) as a target destination", - ), - ).not.toBeVisible(); - - // Wait for the memo button to be enabled - const memoButton = page.getByTestId("send-amount-btn-memo"); - await expect(memoButton).toBeEnabled({ timeout: 10000 }); - - // Click memo button - memo should be disabled because destination is M address - // (memo is encoded in M address, so it's disabled) - await memoButton.click(); - await expect(page.getByTestId("edit-memo-input")).toBeVisible({ - timeout: 10000, - }); - await expect(page.getByTestId("edit-memo-input")).toBeDisabled(); - // Wait for the disabled message to appear - // The message should appear once the contract check completes - await expect( - page.getByText("Memo is disabled for this transaction"), - ).toBeVisible({ timeout: 15000 }); - await page.getByText("Cancel").click(); - - // Click Review Send - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); - await reviewSendButton.click({ force: true }); - - // Wait for review sheet to open - await expect(page.getByText("You are sending")).toBeVisible({ - timeout: 200000, - }); - - // Verify memo row is NOT shown in review (memo is embedded in M address for tokens with Soroban mux support) - await expect(page.getByTestId("review-tx-memo")).not.toBeVisible(); -}); - -test("Send token payment from Asset Detail", async ({ page, extensionId }) => { - test.slow(); - - await stubAccountBalancesE2e(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubSimulateTokenTransfer(page); - await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); - - await loginToTestAccount({ page, extensionId }); - 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("send-amount-amount-input").fill("0.01"); await page.getByTestId("address-tile").click(); await page @@ -1724,13 +743,7 @@ test("Send token payment from Asset Detail", async ({ page, extensionId }) => { await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); 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", + "0.01", ); const reviewSendButton = page.getByTestId("send-amount-btn-continue"); @@ -1745,42 +758,11 @@ test("Send token payment from Asset Detail", async ({ page, extensionId }) => { await expect(page.getByTestId("SubmitAction")).toBeEnabled(); }); -test("Send XLM payment from Asset Detail", async ({ page, extensionId }) => { - test.slow(); - - await stubAccountBalancesE2e(page); - await stubAccountHistory(page); - await stubTokenDetails(page); - await stubTokenPrices(page); - await stubSimulateTokenTransfer(page); - await stubContractSpec(page, TEST_TOKEN_ADDRESS, true); - - await loginToTestAccount({ page, extensionId }); - 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 page - .getByTestId("send-to-input") - .fill("GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY"); - await page.getByText("Continue").click(); - - await expect(page.getByTestId("send-amount-amount-input")).toBeVisible(); - await expect(page.getByTestId("send-amount-amount-input")).toHaveValue( - "0.01", - ); - - const reviewSendButton = page.getByTestId("send-amount-btn-continue"); - await expect(reviewSendButton).toBeEnabled({ timeout: 10000 }); - await reviewSendButton.click({ force: true }); - - await expect(page.getByText("You are sending")).toBeVisible(); - - await expect(page.getByTestId("SubmitAction")).toBeVisible({ - timeout: 15000, +// Reset environment variables before each memo-related test +// This ensures IS_PLAYWRIGHT is set for memo validation bypass +test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + // Ensure IS_PLAYWRIGHT is set for memo validation bypass + (window as any).IS_PLAYWRIGHT = "true"; }); - await expect(page.getByTestId("SubmitAction")).toBeEnabled(); }); diff --git a/extension/e2e-tests/test-fixtures.ts b/extension/e2e-tests/test-fixtures.ts index 639450c1d5..a1790cc916 100644 --- a/extension/e2e-tests/test-fixtures.ts +++ b/extension/e2e-tests/test-fixtures.ts @@ -1,10 +1,15 @@ -import { test as base, chromium, BrowserContext, Page } from "@playwright/test"; +import { + test as base, + chromium, + BrowserContext, + Page, + Worker, +} from "@playwright/test"; import path from "path"; -import { STELLAR_EXPERT_ASSET_LIST_JSON } from "./helpers/stubs"; - export const test = base.extend<{ context: BrowserContext; + serviceWorker: Worker; extensionId: string; page: Page; language: string; @@ -70,17 +75,28 @@ export const test = base.extend<{ langsValue: config.langs, }, ); - - if (!process.env.IS_INTEGRATION_MODE) { - await page.route("*/**/testnet/asset-list/top50", async (route) => { - const json = STELLAR_EXPERT_ASSET_LIST_JSON; - await route.fulfill({ json }); - }); - } use(page); }, }); +test.afterEach(async ({ page, context }) => { + // Clean up all route handlers to avoid teardown timeout issues + try { + await page.unroute("**/*"); + await context.unroute("**/*"); + const pages = context.pages(); + await Promise.all( + pages.map(async (p) => { + if (!p.isClosed()) { + await p.close(); + } + }), + ); + } catch (e) { + // Silently ignore errors during cleanup + } +}); + export const expectPageToHaveScreenshot = async ( { page, diff --git a/extension/e2e-tests/translations-pt.test.ts b/extension/e2e-tests/translations-pt.test.ts index 131388bea8..9182153705 100644 --- a/extension/e2e-tests/translations-pt.test.ts +++ b/extension/e2e-tests/translations-pt.test.ts @@ -1,10 +1,11 @@ import { test, expect } from "./test-fixtures"; import { loginToTestAccountPT } from "./helpers/login-pt"; - +import { stubAllExternalApis } from "./helpers/stubs"; test.use({ language: "pt" }); -test.beforeEach(async ({ page, extensionId }) => { +test.beforeEach(async ({ page, extensionId, context }) => { await page.goto(`chrome-extension://${extensionId}/index.html`); + await stubAllExternalApis(page, context); }); test("Smoke test: Portuguese translations are loaded", async ({ page }) => { diff --git a/extension/package.json b/extension/package.json index 67fd516e88..5031174fcb 100644 --- a/extension/package.json +++ b/extension/package.json @@ -93,7 +93,7 @@ }, "devDependencies": { "@lavamoat/allow-scripts": "3.3.2", - "@playwright/test": "1.49.0", + "@playwright/test": "1.57.0", "@sentry/webpack-plugin": "3.3.1", "@svgr/webpack": "8.1.0", "@types/js-yaml": "4.0.9", diff --git a/extension/playwright.config.ts b/extension/playwright.config.ts index 3224775e10..de8b053462 100644 --- a/extension/playwright.config.ts +++ b/extension/playwright.config.ts @@ -16,12 +16,14 @@ export default defineConfig({ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 5 : 0, - /* Fail the build if any test fails afte retries */ + retries: 5, + /* Fail the build if any test fails after retries */ maxFailures: 1, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + /* In integration mode, run tests sequentially to avoid conflicts */ + workers: process.env.IS_INTEGRATION_MODE ? 1 : process.env.CI ? 4 : 8, + /* Increase worker teardown timeout to handle route cleanup */ + webServer: undefined, + globalTimeout: process.env.CI ? 3600000 : 600000, // 1 hour on CI, 10 min locally /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "list", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -30,7 +32,7 @@ export default defineConfig({ // baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", + trace: "retain-on-failure", launchOptions: { env: { ...process.env, diff --git a/package.json b/package.json index 12e0eabb7f..eef3888ad4 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "start:unpacked": "yarn workspace extension start:unpacked-extension", "install-if-package-changed": "git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep --quiet yarn.lock && yarn setup || exit 0", "test:ci": "jest --ci", - "test:e2e": "yarn workspace extension test:e2e --workers=1", + "test:e2e": "yarn workspace extension test:e2e", "test": "jest -o --watch", "prepare": "husky install" }, diff --git a/yarn.lock b/yarn.lock index f0cb5a1a8b..ab204adf3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4443,14 +4443,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:1.49.0": - version: 1.49.0 - resolution: "@playwright/test@npm:1.49.0" +"@playwright/test@npm:1.57.0": + version: 1.57.0 + resolution: "@playwright/test@npm:1.57.0" dependencies: - playwright: "npm:1.49.0" + playwright: "npm:1.57.0" bin: playwright: cli.js - checksum: 10c0/2890d52ee45bd83b5501f17a77c77f12ba934d257fda4b288405c6d91f94b83c4fcbdff3c0be89c2aaeea3d13576b72ec9a70be667ff844b342044afd72a246e + checksum: 10c0/35ba4b28be72bf0a53e33dbb11c6cff848fb9a37f49e893ce63a90675b5291ec29a1ba82c8a3b043abaead129400f0589623e9ace2e6a1c8eaa409721ecc3774 languageName: node linkType: hard @@ -11323,7 +11323,7 @@ __metadata: "@lavamoat/allow-scripts": "npm:3.3.2" "@ledgerhq/hw-app-str": "npm:7.2.0" "@ledgerhq/hw-transport-webhid": "npm:6.30.6" - "@playwright/test": "npm:1.49.0" + "@playwright/test": "npm:1.57.0" "@radix-ui/react-dialog": "npm:1.1.15" "@radix-ui/react-popover": "npm:1.1.15" "@reduxjs/toolkit": "npm:2.5.0" @@ -18048,27 +18048,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.49.0": - version: 1.49.0 - resolution: "playwright-core@npm:1.49.0" +"playwright-core@npm:1.57.0": + version: 1.57.0 + resolution: "playwright-core@npm:1.57.0" bin: playwright-core: cli.js - checksum: 10c0/22c1a72fabdcc87bd1cd4d40a032d2c5b94cf94ba7484dc182048c3fa1c8ec26180b559d8cac4ca9870e8fd6bdf5ef9d9f54e7a31fd60d67d098fcffc5e4253b + checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9 languageName: node linkType: hard -"playwright@npm:1.49.0": - version: 1.49.0 - resolution: "playwright@npm:1.49.0" +"playwright@npm:1.57.0": + version: 1.57.0 + resolution: "playwright@npm:1.57.0" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.49.0" + playwright-core: "npm:1.57.0" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/e94d662747cd147d0573570fec90dadc013c1097595714036fc8934a075c5a82ab04a49111b03b1f762ea86429bdb7c94460901896901e20970b30ce817cc93f + checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899 languageName: node linkType: hard