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
5,047 changes: 1,951 additions & 3,096 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,54 @@ describe("AccountNameEditRow", () => {

expect(screen.getByTestId("DeleteAddressMessage")).toBeInTheDocument();
});

it("should call onConfirmDelete when confirm button clicked", async () => {
const onConfirmDelete = vi.fn();
render(
<AccountNameEditRow
profile={profile}
wallets={profile.wallets().values()}
accountName="Test Account"
isDeleting={true}
onConfirmDelete={onConfirmDelete}
/>,
);

await userEvent.click(screen.getByTestId("ConfirmDelete"));
expect(onConfirmDelete).toHaveBeenCalled();
});

it("should call onCancelDelete and reset editing state when cancel button clicked", async () => {
const onCancelDelete = vi.fn();
render(
<AccountNameEditRow
profile={profile}
wallets={profile.wallets().values()}
accountName="Test Account"
isDeleting={true}
onCancelDelete={onCancelDelete}
/>,
);

await userEvent.click(screen.getByTestId("CancelDelete"));
expect(onCancelDelete).toHaveBeenCalled();
});

it("should call onCancel when cancel button clicked in UpdateAccountName", async () => {
render(
<AccountNameEditRow profile={profile} wallets={profile.wallets().values()} accountName="Test Account" />,
);

await userEvent.click(screen.getByTestId("AccountNameEditRow__edit"));

await waitFor(() => {
expect(screen.getByTestId("UpdateWalletName__input")).toBeInTheDocument();
});

await userEvent.click(screen.getByTestId("UpdateWalletName__cancel"));

await waitFor(() => {
expect(screen.queryByTestId("UpdateWalletName__input")).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const AccountNameEditRow = ({
<Button
variant="transparent"
size="md"
data-testid="AccountNameEditRow__delete"
className="h-6 w-6 p-0"
onClick={() => {
setIsEditing(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,24 @@ describe("AddressRow", () => {
expect(screen.getByText("Edit content")).toBeInTheDocument();
});

it("should render error message", () => {
render(
<AddressRow
profile={profile}
wallet={wallet}
onDelete={vi.fn()}
usesManageMode={false}
toggleAddress={vi.fn()}
isSelected={false}
onEdit={vi.fn()}
isError={true}
errorMessage="Test error message"
/>,
);

expect(screen.getByText("Test error message")).toBeInTheDocument();
});

it("should should trigger onEdit", async () => {
const onEdit = vi.fn();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export const AddressRow = ({
<Button
size="icon"
variant="transparent"
data-testid={`AddressRow--dropdown-${wallet.address()}`}
className={cn("ml-4 p-1", {
"hover:bg-theme-navy-200 dim-hover:bg-theme-dim-800 dark:hover:bg-theme-secondary-700":
!isEditing && !deleteContent,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import { Contracts } from "@/app/lib/profiles";
import { env, getMainsailProfileId, render } from "@/utils/testing-library";
import { env, getMainsailProfileId, render, MNEMONICS } from "@/utils/testing-library";
import { expect } from "vitest";
import { AddressesSidePanel } from "./AddressesSidePanel";
import userEvent from "@testing-library/user-event";
Expand Down Expand Up @@ -141,6 +141,28 @@ describe("AddressesSidePanel", () => {
getItemSpy.mockRestore();
});

it("should dismiss manage hint when clicking Got it", async () => {
const getItemSpy = vi.spyOn(Storage.prototype, "getItem").mockReturnValue(undefined);

render(<AddressesSidePanel open={true} onClose={vi.fn()} onOpenChange={vi.fn()} />, {
route: `/profiles/${fixtureProfileId}/dashboard`,
});

await waitFor(
() => {
expect(screen.getByText(/You can manage and remove your addresses here./)).toBeVisible();
},
{ timeout: 4000 },
);

await userEvent.click(screen.getByTestId("HideManageHint"));

await waitFor(() => {
expect(screen.queryByText(/You can manage and remove your addresses here./)).not.toBeInTheDocument();
});

getItemSpy.mockRestore();
});
it("should not show a hint for `manage` button if already shown", async () => {
const getItemSpy = vi.spyOn(Storage.prototype, "getItem").mockReturnValue("1");

Expand Down Expand Up @@ -249,4 +271,166 @@ describe("AddressesSidePanel", () => {
// should reset back to select mode
expect(screen.getByTestId("ManageAddresses")).toBeInTheDocument();
});

it("should not toggle address when in manage mode", async () => {
const onClose = vi.fn();

render(<AddressesSidePanel open={true} onClose={onClose} onOpenChange={vi.fn()} />, {
route: `/profiles/${fixtureProfileId}/dashboard`,
});

await userEvent.click(screen.getByTestId("ManageAddresses"));
await userEvent.click(screen.getAllByTestId("AddressRow")[0]);

expect(screen.getByTestId("BackManage")).toBeInTheDocument();
});

it("should show edit modal when clicking edit from dropdown", async () => {
render(<AddressesSidePanel open={true} onOpenChange={vi.fn()} />, {
route: `/profiles/${fixtureProfileId}/dashboard`,
});

await userEvent.click(screen.getByTestId("ManageAddresses"));
await userEvent.click(screen.getByTestId(`AddressRow--dropdown-${wallets.first().address()}`));
await userEvent.click(screen.getByText(/Edit/i));

await waitFor(() => {
expect(screen.getByTestId("UpdateWalletName__input")).toBeInTheDocument();
});
});

it("should cancel editing address and reset manage state", async () => {
render(<AddressesSidePanel open={true} onOpenChange={vi.fn()} />, {
route: `/profiles/${fixtureProfileId}/dashboard`,
});

await userEvent.click(screen.getByTestId("ManageAddresses"));
await userEvent.click(screen.getByTestId(`AddressRow--dropdown-${wallets.first().address()}`));
await userEvent.click(screen.getByText(/Edit/i));

await waitFor(() => {
expect(screen.getByTestId("UpdateWalletName__input")).toBeInTheDocument();
});

await userEvent.click(screen.getByTestId("UpdateWalletName__cancel"));

await waitFor(() => {
expect(screen.queryByTestId("UpdateWalletName__input")).not.toBeInTheDocument();
});

expect(screen.getByTestId("ManageAddresses")).toBeInTheDocument();
});

it("should save edited address name", async () => {
render(<AddressesSidePanel open={true} onOpenChange={vi.fn()} />, {
route: `/profiles/${fixtureProfileId}/dashboard`,
});

await userEvent.click(screen.getByTestId("ManageAddresses"));
await userEvent.click(screen.getByTestId(`AddressRow--dropdown-${wallets.first().address()}`));
await userEvent.click(screen.getByText(/Edit/i));

await waitFor(() => {
expect(screen.getByTestId("UpdateWalletName__input")).toBeInTheDocument();
});

const input = screen.getByTestId("UpdateWalletName__input") as HTMLInputElement;
await userEvent.clear(input);
await userEvent.type(input, "New Name");

await userEvent.click(screen.getByTestId("UpdateWalletName__submit"));

await waitFor(() => {
expect(screen.queryByTestId("UpdateWalletName__input")).not.toBeInTheDocument();
});
});

it("should hit closeSidepanel guard when panel closes via external trigger", async () => {
const onOpenChange = vi.fn((open) => {
if (!open) {
return;
}
});

render(<AddressesSidePanel open={true} onClose={() => {}} onOpenChange={onOpenChange} />, {
route: `/profiles/${fixtureProfileId}/dashboard`,
});

await userEvent.click(screen.getByTestId("SelectAllAddresses"));

expect(screen.getAllByTestId("AddressRow")).toHaveLength(wallets.count()); // panel renders correctly before closing

await userEvent.keyboard("[Escape]");
});

it("should show empty block when no wallets match the current filter", async () => {
render(<AddressesSidePanel open={true} onClose={vi.fn()} onOpenChange={() => {}} onMountChange={() => {}} />, {
route: `/profiles/${fixtureProfileId}/dashboard`,
});

await userEvent.clear(getSearchInput());
await waitFor(() => {
expect(screen.getAllByTestId("AddressRow").length).toBeGreaterThan(0); // verify initial render has rows
});

await userEvent.type(getSearchInput(), "onexistent_address");

await waitFor(
() => {
expect(screen.queryByTestId("AddressRow")).not.toBeInTheDocument();
},
{ timeout: 3000 },
);
});

it("should trigger toggleAddress callback when clicking an address row in single view", async () => {
const onClose = vi.fn();

render(<AddressesSidePanel open={true} onClose={onClose} onOpenChange={vi.fn()} />, {
route: `/profiles/${fixtureProfileId}/dashboard`,
});

const singleTabButton = screen.getByTestId("tabs__tab-button-single");
await userEvent.click(singleTabButton);

const addressRows = screen.getAllByTestId("AddressRow");
await userEvent.click(addressRows[0]);

expect(screen.getByTestId(sidePanelCloseButton)).toBeInTheDocument();
});

it("should handle HD wallet account delete", async () => {
const mnemonic = MNEMONICS[0];
const hdWallet = await profile.walletFactory().fromMnemonicWithBIP44({
levels: { account: 0 },
mnemonic,
});

hdWallet.mutator().accountName("Test HD Account");
profile.wallets().push(hdWallet);

render(<AddressesSidePanel open={true} onOpenChange={vi.fn()} />, {
route: `/profiles/${fixtureProfileId}/dashboard`,
});

await waitFor(() => {
expect(screen.getByTestId("hd-wallet-label")).toBeInTheDocument();
});

await userEvent.click(screen.getByTestId("ManageAddresses"));
await waitFor(() => {
expect(screen.getByTestId("AccountNameEditRow__wrapper")).toBeInTheDocument();
});

const deleteButton = screen.getByTestId("AccountNameEditRow__delete");
await userEvent.click(deleteButton);

await waitFor(() => {
expect(screen.getByTestId("DeleteAddressMessage")).toBeInTheDocument();
});

await userEvent.click(screen.getByTestId("CancelDelete"));

profile.wallets().forget(hdWallet.id());
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,38 @@ import { groupBy, sortBy } from "@/app/lib/helpers";
import { AccountNameEditRow } from "./AccountNameEditRow";
import { Contracts } from "@/app/lib/profiles";

export const computeWalletErrorState = ({
isManageMode,
selectedAddressesLength,
addressToDelete,
hdAccountToDelete,
wallet,
}: {
isManageMode: boolean;
selectedAddressesLength: number;
addressToDelete: string | undefined;
hdAccountToDelete: string | undefined;
wallet: Contracts.IReadWriteWallet;
}): boolean => {
if (!isManageMode) {
return false;
}

if (selectedAddressesLength === 0) {
return false;
}

if (wallet.address() === addressToDelete) {
return true;
}

if (hdAccountToDelete && hdAccountToDelete === wallet.accountName()) {
return true;
}

return false;
};

export const AddressesSidePanel = ({
open,
onClose,
Expand Down Expand Up @@ -115,6 +147,7 @@ export const AddressesSidePanel = ({

const groupedByAccountName = sortBy(
Object.entries(groupBy(addressesToShow, (w) => w.accountName() ?? undefined)),
/* istanbul ignore next -- @preserve sortBy comparator */
([key]) => [key === undefined, key ?? ""],
);

Expand All @@ -137,25 +170,14 @@ export const AddressesSidePanel = ({
return;
};

const renderErrorState = (wallet: Contracts.IReadWriteWallet) => {
if (!isManageMode) {
return false;
}

if (selectedAddresses.length === 0) {
return false;
}

if (wallet.address() === addressToDelete) {
return true;
}

if (!!hdAccountToDelete && hdAccountToDelete === wallet.accountName()) {
return true;
}

return false;
};
const renderErrorState = (wallet: Contracts.IReadWriteWallet) =>
computeWalletErrorState({
addressToDelete,
hdAccountToDelete,
isManageMode,
selectedAddressesLength: selectedAddresses.length,
wallet,
});

return (
<SidePanel
Expand Down Expand Up @@ -352,6 +374,7 @@ export const AddressesSidePanel = ({
setHdAccountNameToDelete?.(undefined);
}}
onConfirmDelete={() => {
/* istanbul ignore next -- @preserve HD account delete triggers actual wallet deletion */
Promise.all(
wallets.map((wallet: Contracts.IReadWriteWallet) => handleDelete(wallet)),
);
Expand All @@ -378,6 +401,7 @@ export const AddressesSidePanel = ({
}

// Automatically close if single mode.
/* istanbul ignore next -- @preserve single-mode auto-close depends on async profile setting sync */
const newSelection = toggleSelection(wallet);
if (profile.walletSelectionMode() === "single") {
closeSidepanel(newSelection);
Expand Down
Loading
Loading