Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions extension/e2e-tests/addAssetContractSearch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Page } from "@playwright/test";
import { test, expect } from "./test-fixtures";
import { loginToTestAccount } from "./helpers/login";
import {
stubTokenDetails,
stubIsSac,
stubScanAssetSafe,
stubAssetSearchWithContractId,
stubAccountBalancesE2e,
} from "./helpers/stubs";

/**
* Helper to locate a ManageAssetRow by its exact asset code.
*/
const getAssetRow = (page: Page, code: string) =>
page.getByTestId("ManageAssetRow").filter({
has: page.getByTestId("ManageAssetCode").getByText(code, { exact: true }),
});

test("Stellar Expert contract ID result shows as already added", async ({
page,
extensionId,
context,
}) => {
test.slow();

await loginToTestAccount({
page,
extensionId,
context,
stubOverrides: async () => {
await stubAssetSearchWithContractId(page);
await stubAccountBalancesE2e(page);
await stubTokenDetails(page);
await stubIsSac(page);
await stubScanAssetSafe(page);
Comment on lines +31 to +36
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These stubs are added after stubAllExternalApis, which already registers route handlers for both **/asset?search** and **/account-balances/**. Without calling page.unroute(...) first, these overrides may not take effect (other tests override default stubs by unrouting first). Unroute the existing handlers before registering stubAssetSearchWithContractId / stubAccountBalancesE2e to avoid flaky or failing expectations.

Copilot uses AI. Check for mistakes.
},
});

await page.getByTestId("account-options-dropdown").click();
const manageAssets = page.getByText("Manage assets");
await expect(manageAssets).toBeVisible();
await manageAssets.click();

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("E2E");

// Wait for search results to appear
const rows = page.getByTestId("ManageAssetRow");
await expect(rows.first()).toBeVisible({ timeout: 10000 });

// The E2E token row should show the ellipsis menu instead of "Add"
// because the token is already in the user's balances
await expect(
page.getByTestId("ManageAssetRowButton__ellipsis-E2E"),
).toBeVisible();
});

test("Stellar Expert contract ID result shows Add when not owned", async ({
page,
extensionId,
context,
}) => {
test.slow();

await loginToTestAccount({
page,
extensionId,
context,
stubOverrides: async () => {
await stubAssetSearchWithContractId(page);
await stubTokenDetails(page);
await stubIsSac(page);
await stubScanAssetSafe(page);
Comment on lines +72 to +76
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stubAllExternalApis already stubs **/asset?search**. To ensure this contract-ID version is actually used, unroute the existing handler (e.g., await page.unroute("**/asset?search**")) before calling stubAssetSearchWithContractId(page) in this override.

Copilot uses AI. Check for mistakes.
},
});

await page.getByTestId("account-options-dropdown").click();
const manageAssets = page.getByText("Manage assets");
await expect(manageAssets).toBeVisible();
await manageAssets.click();

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("E2E");

// Wait for search results
const rows = page.getByTestId("ManageAssetRow");
await expect(rows.first()).toBeVisible({ timeout: 10000 });

// Find the E2E token row by its exact asset code
const e2eRow = getAssetRow(page, "E2E");
await expect(e2eRow).toBeVisible();

// The button should say "Add" since the user does not have this token
const rowButton = e2eRow.getByTestId("ManageAssetRowButton");
await expect(rowButton).toHaveText("Add");
});

test("Can add a token returned as contract ID from Stellar Expert search", async ({
page,
extensionId,
context,
}) => {
test.slow();

await loginToTestAccount({
page,
extensionId,
context,
stubOverrides: async () => {
await stubAssetSearchWithContractId(page);
await stubTokenDetails(page);
await stubIsSac(page);
await stubScanAssetSafe(page);
Comment on lines +114 to +118
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stubAllExternalApis already stubs **/asset?search**. To ensure this contract-ID version is actually used, unroute the existing handler (e.g., await page.unroute("**/asset?search**")) before calling stubAssetSearchWithContractId(page) in this override.

Copilot uses AI. Check for mistakes.
},
});

await page.getByTestId("account-options-dropdown").click();
const manageAssets = page.getByText("Manage assets");
await expect(manageAssets).toBeVisible();
await manageAssets.click();

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("E2E");

// Wait for search results
const rows = page.getByTestId("ManageAssetRow");
await expect(rows.first()).toBeVisible({ timeout: 10000 });

// Find the E2E token row by its exact asset code and click Add
const e2eRow = getAssetRow(page, "E2E");
await expect(e2eRow).toBeVisible();
await e2eRow.getByTestId("ManageAssetRowButton").click();

// Should navigate to the Add Token confirmation page
await expect(page.getByTestId("ToggleToken__asset-code")).toHaveText(
"E2E Token",
);
await expect(page.getByTestId("ToggleToken__asset-add-remove")).toHaveText(
"Add Token",
);

// Confirm the add
await page.getByRole("button", { name: "Confirm" }).click();
});
33 changes: 33 additions & 0 deletions extension/e2e-tests/helpers/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,39 @@ export const stubAssetSearch = async (page: Page) => {
});
};

export const stubAssetSearchWithContractId = async (page: Page) => {
await page.route("**/asset?search**", async (route) => {
const json = {
_embedded: {
records: [
{
asset:
"USDC-GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
},
{
asset: TEST_TOKEN_ADDRESS,
code: "E2E",
token_name: "E2E Token",
decimals: 3,
domain: "example.com",
tomlInfo: {
code: "E2E",
// Use a different address than the token contract to match real
// Stellar Expert responses where tomlInfo.issuer is the token
// issuer, not the token contract itself.
issuer:
"GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
name: "E2E Token",
image: "",
},
},
],
},
};
await route.fulfill({ json });
});
};

export const stubHorizonAccounts = async (page: Page) => {
await page.route("**/accounts/**", async (route) => {
await route.fulfill({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ import { AppDispatch, store } from "popup/App";
interface AssetRecord {
asset: string;
domain?: string;
tomlInfo?: { image: string };
code?: string;
token_name?: string;
decimals?: number;
tomlInfo?: {
image?: string;
code?: string;
issuer?: string;
name?: string;
};
}

interface AssetLookupDetails {
Expand Down Expand Up @@ -257,7 +265,8 @@ const useAssetLookup = () => {

/*
* Fetches data from Stellar Expert for the given asset.
* It returns an array of ManageAssetCurrency objects.
* Returns an array of ManageAssetCurrency objects for both classic
* ({code}-{issuer}) and contract ID results.
*
* @param {string} asset - The asset to look up.
* @returns {Promise<ManageAssetCurrency[]>}
Expand All @@ -282,13 +291,27 @@ const useAssetLookup = () => {
throw new Error("Failed to fetch Stellar Expert data");
});

return resJson._embedded.records.map((record: AssetRecord) => ({
code: record.asset.split("-")[0],
issuer: record.asset.split("-")[1],
domain: record.domain,
image: record.tomlInfo?.image,
isSuspicious: false,
}));
return resJson._embedded.records.map((record: AssetRecord) => {
if (isContractId(record.asset)) {
return {
code: record.code || record.tomlInfo?.code || "",
issuer: record.asset,
contract: record.asset,
Comment on lines +295 to +299
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Stellar Expert search mapping now emits contract-ID assets (issuer/contract both set to the contract ID). Downstream in this hook, BlockAid bulk scanning builds asset_ids as ${code}-${issuer} for all returned rows when the search term is not a contract ID; that will end up scanning code-<contractId> for these results, which the existing logic/comments indicate is intended only for classic assets. Consider filtering contract-ID rows out of the BlockAid scan path (e.g., skip rows where isContractId(row.issuer) / row.contract is set) or updating the scan identifier format if BlockAid supports contract assets.

Copilot uses AI. Check for mistakes.
domain: record.domain ?? null,
image: record.tomlInfo?.image,
name: record.token_name || record.tomlInfo?.name,
decimals: record.decimals,
isSuspicious: false,
};
}
return {
code: record.asset.split("-")[0],
issuer: record.asset.split("-")[1],
domain: record.domain ?? null,
image: record.tomlInfo?.image,
isSuspicious: false,
};
});
};

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { getSwapErrorMessage, ERROR_TO_DISPLAY } from "../useSimulateSwapData";

const CONTRACT_ID = "CAZXEHTSQATVQVWDPWWDTFSY6CM764JD4MZ6HUVPO3QKS64QEEP4KJH7";
const CLASSIC_ISSUER =
"GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5";

const classicAsset = { issuer: CLASSIC_ISSUER };
const contractAsset = { issuer: CONTRACT_ID };

describe("getSwapErrorMessage", () => {
it("returns custom token error when source is a contract ID", () => {
const result = getSwapErrorMessage(
new Error("some error"),
contractAsset,
classicAsset,
);
expect(result).toBe(ERROR_TO_DISPLAY.CUSTOM_TOKEN_NOT_SUPPORTED);
});

it("returns custom token error when dest is a contract ID", () => {
const result = getSwapErrorMessage(
new Error("some error"),
classicAsset,
contractAsset,
);
expect(result).toBe(ERROR_TO_DISPLAY.CUSTOM_TOKEN_NOT_SUPPORTED);
});

it("returns custom token error when both are contract IDs", () => {
const result = getSwapErrorMessage(
new Error("some error"),
contractAsset,
contractAsset,
);
expect(result).toBe(ERROR_TO_DISPLAY.CUSTOM_TOKEN_NOT_SUPPORTED);
});

it("returns known error even when assets are contract IDs", () => {
const result = getSwapErrorMessage(
new Error(ERROR_TO_DISPLAY.NO_PATH_FOUND),
contractAsset,
classicAsset,
);
expect(result).toBe(ERROR_TO_DISPLAY.NO_PATH_FOUND);
});

it("returns known error message for classic assets", () => {
const result = getSwapErrorMessage(
new Error(ERROR_TO_DISPLAY.NO_PATH_FOUND),
classicAsset,
classicAsset,
);
expect(result).toBe(ERROR_TO_DISPLAY.NO_PATH_FOUND);
});

it("returns unknown error for unrecognized Error with classic assets", () => {
const result = getSwapErrorMessage(
new Error("something unexpected"),
classicAsset,
classicAsset,
);
expect(result).toBe(
"We had an issue retrieving your transaction details. Please try again.",
);
});
Comment on lines +61 to +65
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts the full unknown-error string literal, which is brittle if copy changes. Consider exporting the unknown-error constant from the hook module or asserting more flexibly (e.g., stable substring) to keep the test focused on behavior rather than exact wording.

Copilot uses AI. Check for mistakes.

it("returns known error message for string errors with classic assets", () => {
const result = getSwapErrorMessage(
ERROR_TO_DISPLAY.NO_PATH_FOUND,
classicAsset,
classicAsset,
);
expect(result).toBe(ERROR_TO_DISPLAY.NO_PATH_FOUND);
});

it("returns unknown error for unrecognized string with classic assets", () => {
const result = getSwapErrorMessage(
"something unexpected",
classicAsset,
classicAsset,
);
expect(result).toBe(
"We had an issue retrieving your transaction details. Please try again.",
);
});

it("returns unknown error for non-Error non-string with classic assets", () => {
const result = getSwapErrorMessage(42, classicAsset, classicAsset);
expect(result).toBe(
"We had an issue retrieving your transaction details. Please try again.",
);
});
});
Loading
Loading