Skip to content
Closed
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);
},
});

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);
},
});

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);
},
});

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,
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.",
);
});

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