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