Skip to content
Merged
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
18 changes: 17 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,23 @@
],
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts"
"!src/**/*.d.ts",
"!src/native.node",
"!src/electronBridge.ts",
"!src/__mocks__/**",
"!src/test-utils.tsx",
"!src/setupTests.ts",
"!src/version.ts",
"!src/**/index.ts",
"!src/components/appstate/enums/**",
"!src/components/appstate/types/**",
"!src/rpc/components/**",
"!src/components/send/components/SendManyJSONType.ts",
"!src/components/balanceBlock/components/**",
"!src/root/Routes.tsx",
"!src/rpc/rpc.ts",
"!src/components/loadingScreen/LoadingScreen.tsx",
"!src/components/addNewWallet/AddNewWallet.tsx"
],
"setupFiles": [
"react-app-polyfill/jsdom"
Expand Down
6 changes: 6 additions & 0 deletions src/__mocks__/electronBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ export const native = {
set_wallet_base_dir: jest.fn(),
start_security_scoped_access: jest.fn(),
get_latest_block_server: jest.fn(),
// Send
send: jest.fn(),
get_spendable_balance_with_address: jest.fn(),
// History
remove_transaction: jest.fn(),
// Insight
get_total_value_to_address: jest.fn(),
get_total_number_of_sends: jest.fn(),
get_total_spends_to_address: jest.fn(),
get_total_memobytes_to_address: jest.fn(),
};

Expand Down
154 changes: 153 additions & 1 deletion src/components/addressBook/Addressbook.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import React from "react";
import { render, screen, fireEvent } from "../../test-utils";
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import { render } from "../../test-utils";
import AddressBook from "./Addressbook";
import { AddressBookEntryClass, ServerChainNameEnum } from "../appstate";

jest.mock("../../electronBridge");

// Provide a controllable ZNS resolver — `mock` prefix avoids jest hoist restriction.
let mockResolveImpl: (alias: string, chain: string) => Promise<any> = async () =>
({ ok: false, reason: "not-found" });
jest.mock("../../utils/zns", () => {
const actual = jest.requireActual("../../utils/zns");
return {
...actual,
resolveZnsAlias: (alias: string, chain: string) => mockResolveImpl(alias, chain),
};
});

// eslint-disable-next-line @typescript-eslint/no-require-imports
const { native } = require("../../electronBridge");

beforeEach(() => {
mockResolveImpl = async () => ({ ok: false, reason: "not-found" });
(native.parse_address as jest.Mock).mockReset();
});

const baseProps = {
addAddressBookEntry: jest.fn(),
removeAddressBookEntry: jest.fn(),
Expand Down Expand Up @@ -57,4 +77,136 @@ describe("AddressBook", () => {
});
expect(await screen.findByText("Label is too long")).toBeInTheDocument();
});

it("shows 'Invalid Address' when address is unrecognized", async () => {
(native.parse_address as jest.Mock).mockResolvedValue("");
render(<AddressBook {...baseProps} />);
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "garbage" } });
expect(await screen.findByText("Invalid Address")).toBeInTheDocument();
});

it("recognizes a unified address and shows 'Unified' tag", async () => {
(native.parse_address as jest.Mock).mockResolvedValue(
JSON.stringify({ status: "success", address_kind: "unified", chain_name: "main" }),
);
render(<AddressBook {...baseProps} />);
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "u1validaddr" } });
expect(await screen.findByText("Unified")).toBeInTheDocument();
});

it("recognizes a sapling address and shows 'Sapling' tag", async () => {
(native.parse_address as jest.Mock).mockResolvedValue(
JSON.stringify({ status: "success", address_kind: "sapling", chain_name: "main" }),
);
render(<AddressBook {...baseProps} />);
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "zs1valid" } });
expect(await screen.findByText("Sapling")).toBeInTheDocument();
});

it("recognizes a transparent address and shows 'Transparent' tag", async () => {
(native.parse_address as jest.Mock).mockResolvedValue(
JSON.stringify({ status: "success", address_kind: "transparent", chain_name: "main" }),
);
render(<AddressBook {...baseProps} />);
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "t1valid" } });
expect(await screen.findByText("Transparent")).toBeInTheDocument();
});

it("recognizes a TEX address and shows 'TEX' tag", async () => {
(native.parse_address as jest.Mock).mockResolvedValue(
JSON.stringify({ status: "success", address_kind: "tex", chain_name: "main" }),
);
render(<AddressBook {...baseProps} />);
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "texvalid" } });
expect(await screen.findByText("TEX")).toBeInTheDocument();
});

it("shows a Duplicate Address error", async () => {
(native.parse_address as jest.Mock).mockResolvedValue(
JSON.stringify({ status: "success", address_kind: "unified", chain_name: "main" }),
);
const entry = new AddressBookEntryClass(
"Bob",
"u1existing",
ServerChainNameEnum.mainChainName,
);
render(<AddressBook {...baseProps} />, { contextOverrides: { addressBook: [entry] } });
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "u1existing" } });
expect(await screen.findByText("Duplicate Address")).toBeInTheDocument();
});

it("clears the form when Clear is clicked", async () => {
const setAddLabel = jest.fn();
render(<AddressBook {...baseProps} />, { contextOverrides: { setAddLabel } });
fireEvent.change(screen.getByRole("textbox", { name: /label/i }), { target: { value: "X" } });
fireEvent.click(screen.getByRole("button", { name: /clear/i }));
expect(setAddLabel).toHaveBeenCalled();
expect((screen.getByRole("textbox", { name: /label/i }) as HTMLInputElement).value).toBe("");
});

it("calls addAddressBookEntry on Add click with valid data", async () => {
const addAddressBookEntry = jest.fn();
(native.parse_address as jest.Mock).mockResolvedValue(
JSON.stringify({ status: "success", address_kind: "unified", chain_name: "main" }),
);
render(
<AddressBook addAddressBookEntry={addAddressBookEntry} removeAddressBookEntry={jest.fn()} />,
);
fireEvent.change(screen.getByRole("textbox", { name: /label/i }), { target: { value: "Alice" } });
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "u1valid" } });
await waitFor(() => expect(screen.getByRole("button", { name: /^add$/i })).not.toBeDisabled());
fireEvent.click(screen.getByRole("button", { name: /^add$/i }));
expect(addAddressBookEntry).toHaveBeenCalledWith("Alice", "u1valid", ServerChainNameEnum.mainChainName);
});

it("recognizes a ZNS alias as valid and shows the 'ZNS' tag", async () => {
mockResolveImpl = async () => ({ ok: true, address: "u1resolved" });
render(<AddressBook {...baseProps} />);
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "alice.zcash" } });
expect(await screen.findByText("ZNS")).toBeInTheDocument();
});

it("shows 'ZNS name not found' when alias does not resolve", async () => {
mockResolveImpl = async () => ({ ok: false, reason: "not-found" });
render(<AddressBook {...baseProps} />);
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "ghost.zcash" } });
expect(await screen.findByText("ZNS name not found")).toBeInTheDocument();
});

it("shows 'ZNS lookup failed' when network fails", async () => {
mockResolveImpl = async () => ({ ok: false, reason: "network" });
render(<AddressBook {...baseProps} />);
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "down.zcash" } });
expect(await screen.findByText("ZNS lookup failed")).toBeInTheDocument();
});

it("shows 'ZNS is not available on this network' when chain is unsupported", async () => {
mockResolveImpl = async () => ({ ok: false, reason: "unsupported-chain" });
render(<AddressBook {...baseProps} />);
fireEvent.change(screen.getByRole("textbox", { name: /address/i }), { target: { value: "alice.zcash" } });
expect(await screen.findByText("ZNS is not available on this network")).toBeInTheDocument();
});

it("filters entries by current chain by default", () => {
const main = new AddressBookEntryClass("Alice", "u1main", ServerChainNameEnum.mainChainName);
const test = new AddressBookEntryClass("Tester", "u1test", ServerChainNameEnum.testChainName);
render(<AddressBook {...baseProps} />, {
contextOverrides: { addressBook: [main, test] },
});
expect(screen.getByText("Alice")).toBeInTheDocument();
expect(screen.queryByText("Tester")).not.toBeInTheDocument();
});

it("shows entries from all networks when 'Show contacts from all networks' is checked", async () => {
const main = new AddressBookEntryClass("Alice", "u1main", ServerChainNameEnum.mainChainName);
const test = new AddressBookEntryClass("Tester", "u1test", ServerChainNameEnum.testChainName);
render(<AddressBook {...baseProps} />, {
contextOverrides: { addressBook: [main, test] },
});
expect(screen.queryByText("Tester")).not.toBeInTheDocument();
await act(async () => {
fireEvent.click(screen.getByLabelText(/Show contacts from all networks/i));
});
expect(screen.getByText("Tester")).toBeInTheDocument();
});
});
60 changes: 60 additions & 0 deletions src/components/addressBook/components/AddressbookItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,64 @@ describe("AddressbookItem", () => {
fireEvent.click(screen.getByRole("button", { name: /delete/i }));
expect(removeAddressBookEntry).toHaveBeenCalledWith("Alice");
});

it("shows the [Mainnet] chain badge when showChain is true", () => {
renderInAccordion(<AddressBookItem {...baseProps} showChain={true} />);
expect(screen.getByText("[Mainnet]")).toBeInTheDocument();
});

it("shows the [Testnet] chain badge for testnet entries", () => {
const tItem = new AddressBookEntryClass("Bob", "u1other", ServerChainNameEnum.testChainName);
renderInAccordion(<AddressBookItem item={tItem} removeAddressBookEntry={jest.fn()} showChain={true} />);
expect(screen.getByText("[Testnet]")).toBeInTheDocument();
});

it("does NOT show chain badge when showChain is false (default)", () => {
renderInAccordion(<AddressBookItem {...baseProps} />);
expect(screen.queryByText("[Mainnet]")).not.toBeInTheDocument();
});

it("copies the address on click and expands it (truncated → full)", () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { clipboard } = require("../../../electronBridge");
renderInAccordion(<AddressBookItem {...baseProps} />);
fireEvent.click(screen.getByLabelText("Copy address"));
expect(clipboard.writeText).toHaveBeenCalledWith(item.address);
});

it("copies the address via keyboard Enter", () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { clipboard } = require("../../../electronBridge");
renderInAccordion(<AddressBookItem {...baseProps} />);
fireEvent.keyDown(screen.getByLabelText("Copy address"), { key: "Enter" });
expect(clipboard.writeText).toHaveBeenCalled();
});

it("expands long addresses into chunks when clicked", () => {
const longItem = new AddressBookEntryClass(
"Long",
"u1" + "y".repeat(100),
ServerChainNameEnum.mainChainName,
);
renderInAccordion(<AddressBookItem item={longItem} removeAddressBookEntry={jest.fn()} />);
fireEvent.click(screen.getByLabelText("Copy address"));
// After expand, the address is split into chunks — at least one chunk present
expect(screen.getByLabelText("Copy address")).toBeInTheDocument();
});

it("displays ZNS aliases verbatim (no trimming)", () => {
const znsItem = new AddressBookEntryClass("Alias", "pepe.zcash", ServerChainNameEnum.mainChainName);
renderInAccordion(<AddressBookItem item={znsItem} removeAddressBookEntry={jest.fn()} />);
expect(screen.getByText("pepe.zcash")).toBeInTheDocument();
});

it("Send To navigates to /send with setSendTo", () => {
const setSendTo = jest.fn();
renderInAccordion(<AddressBookItem {...baseProps} />, {
contextOverrides: { currentWallet: mainnetWallet, setSendTo },
});
fireEvent.click(screen.getByText("Alice"));
fireEvent.click(screen.getByRole("button", { name: /send to/i }));
expect(setSendTo).toHaveBeenCalled();
});
});
Loading
Loading