diff --git a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.test.tsx b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.test.tsx new file mode 100644 index 0000000000..170ecf69bc --- /dev/null +++ b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.test.tsx @@ -0,0 +1,100 @@ +import { + getAccountGroupLabel, + mockLedgerAccount, + mockMnemonicAccount, + mockSocialAccount, +} from "@umami/core"; +import { type UmamiStore, accountsActions, addTestAccounts, makeStore } from "@umami/state"; + +import { AccountSelectorModal } from "./AccountSelectorModal"; +import { DeriveMnemonicAccountModal } from "./DeriveMnemonicAccountModal"; +import { + act, + dynamicModalContextMock, + renderInModal, + screen, + userEvent, + waitFor, +} from "../../testUtils"; +import { OnboardOptionsModal } from "../Onboarding/OnboardOptions"; + +let store: UmamiStore; + +beforeEach(() => { + store = makeStore(); + addTestAccounts(store, [mockMnemonicAccount(0), mockLedgerAccount(1), mockSocialAccount(2)]); +}); + +describe("", () => { + it("renders account groups correctly", async () => { + await renderInModal(, store); + + await waitFor(() => + expect(screen.getByText(`Seedphrase ${mockMnemonicAccount(0).seedFingerPrint}`)).toBeVisible() + ); + expect(screen.getByText("Social Accounts")).toBeVisible(); + expect(screen.getByText("Ledger Accounts")).toBeVisible(); + }); + + it.each([ + [ + "mnemonic", + mockMnemonicAccount(0), + () => , + ], + ["ledger", mockLedgerAccount(1), () => ], + ["social", mockSocialAccount(2), () => ], + ])( + "open appropriate modal when clicking 'Add %s Account' button", + async (_, account, getModalComponent) => { + const user = userEvent.setup(); + const accountLabel = getAccountGroupLabel(account); + const { openWith } = dynamicModalContextMock; + await renderInModal(, store); + + await act(() => user.click(screen.getByLabelText(`Add ${accountLabel} account`))); + + expect(openWith).toHaveBeenCalledWith(getModalComponent()); + } + ); + + it("opens appropriate modal when clicking 'Add Account' button", async () => { + const user = userEvent.setup(); + const { openWith } = dynamicModalContextMock; + await renderInModal(, store); + + await act(() => user.click(screen.getByText("Add Account"))); + + expect(openWith).toHaveBeenCalledWith(); + }); + + it("correctly handles account selection", async () => { + const user = userEvent.setup(); + const { onClose } = dynamicModalContextMock; + await renderInModal(, store); + + const accountTile = screen.getByText(mockMnemonicAccount(0).label); + await act(() => user.click(accountTile)); + + expect(store.getState().accounts.current).toBe(mockMnemonicAccount(0).address.pkh); + expect(onClose).toHaveBeenCalled(); + }); + + describe("when account is not verified", () => { + beforeEach(() => { + store.dispatch(accountsActions.setCurrent(mockLedgerAccount(0).address.pkh)); + store.dispatch( + accountsActions.setIsVerified({ + pkh: mockLedgerAccount(0).address.pkh, + isVerified: false, + }) + ); + }); + + it("does not render 'Add Account' button", async () => { + await renderInModal(, store); + + expect(screen.queryByText("Add Account")).not.toBeInTheDocument(); + }); + }); +}); diff --git a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx index 402c5820af..e291ec02f0 100644 --- a/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx +++ b/apps/web/src/components/AccountSelectorModal/AccountSelectorModal.tsx @@ -16,7 +16,7 @@ import { useDynamicModalContext } from "@umami/components"; import { type ImplicitAccount, type MnemonicAccount, getAccountGroupLabel } from "@umami/core"; import { accountsActions, useGetAccountBalance, useImplicitAccounts } from "@umami/state"; import { prettyTezAmount } from "@umami/tezos"; -import { capitalize } from "lodash"; +import { groupBy } from "lodash"; import { useDispatch } from "react-redux"; import { AccountSelectorPopover } from "./AccountSelectorPopover"; @@ -24,7 +24,7 @@ import { PlusIcon, TrashIcon } from "../../assets/icons"; import { useColor } from "../../styles/useColor"; import { AccountTile } from "../AccountTile"; import { ModalCloseButton } from "../CloseButton"; -import { DeriveMnemonicAccountModal } from "../DeriveMnemonicAccountModal"; +import { DeriveMnemonicAccountModal } from "./DeriveMnemonicAccountModal"; import { OnboardOptionsModal } from "../Onboarding/OnboardOptions"; import { useIsAccountVerified } from "../Onboarding/VerificationFlow"; @@ -37,7 +37,7 @@ export const AccountSelectorModal = () => { const dispatch = useDispatch(); - const groupedAccounts = Object.groupBy(accounts, getAccountGroupLabel); + const groupedAccounts = groupBy(accounts, getAccountGroupLabel); const handleDeriveAccount = (account?: ImplicitAccount) => { if (!account) { @@ -48,7 +48,7 @@ export const AccountSelectorModal = () => { case "mnemonic": return openWith(); default: - break; + return openWith(); } }; @@ -74,7 +74,7 @@ export const AccountSelectorModal = () => { paddingLeft="12px" > - {type.split("_").map(capitalize).join(" ")} + {type} { /> } - onClick={() => handleDeriveAccount(accounts?.[0])} + onClick={() => handleDeriveAccount(accounts[0])} size="sm" variant="ghost" /> - {accounts?.map(account => { + {accounts.map(account => { const address = account.address.pkh; const balance = getBalance(address); const onClick = () => { diff --git a/apps/web/src/components/AccountSelectorModal/DeriveMnemonicAccountModal/DeriveMnemonicAccountModal.test.tsx b/apps/web/src/components/AccountSelectorModal/DeriveMnemonicAccountModal/DeriveMnemonicAccountModal.test.tsx new file mode 100644 index 0000000000..fe9613bd9c --- /dev/null +++ b/apps/web/src/components/AccountSelectorModal/DeriveMnemonicAccountModal/DeriveMnemonicAccountModal.test.tsx @@ -0,0 +1,67 @@ +import { mockMnemonicAccount } from "@umami/core"; +import { type UmamiStore, addTestAccount, makeStore, useDeriveMnemonicAccount } from "@umami/state"; + +import { DeriveMnemonicAccountModal } from "./DeriveMnemonicAccountModal"; +import { act, renderInModal, screen, userEvent, waitFor } from "../../../testUtils"; + +let store: UmamiStore; + +jest.mock("@umami/state", () => ({ + ...jest.requireActual("@umami/state"), + useDeriveMnemonicAccount: jest.fn(), +})); + +const mockDeriveMnemonicAccount = jest.fn(); + +beforeEach(() => { + store = makeStore(); + addTestAccount(store, mockMnemonicAccount(0)); + + jest.mocked(useDeriveMnemonicAccount).mockImplementation(() => mockDeriveMnemonicAccount); +}); + +describe("", () => { + it("renders the NameAccountModal with correct subtitle", async () => { + const account = mockMnemonicAccount(0); + await renderInModal(, store); + + await waitFor(() => { + expect(screen.getByText("Name Your Account")).toBeVisible(); + }); + + expect( + screen.getByText(`Name the new account derived from seedphrase ${account.seedFingerPrint}`) + ).toBeVisible(); + }); + + it("handles name submission and opens confirm password modal", async () => { + const user = userEvent.setup(); + + const account = mockMnemonicAccount(0); + await renderInModal(, store); + await act(() => user.type(screen.getByLabelText("Account name"), "Test Account")); + await act(() => user.click(screen.getByRole("button", { name: "Continue" }))); + + await waitFor(() => { + expect(screen.getByTestId("master-password-modal")).toBeVisible(); + }); + }); + + it("derives mnemonic account on password submission", async () => { + const user = userEvent.setup(); + + const account = mockMnemonicAccount(0); + await renderInModal(, store); + + await act(() => user.type(screen.getByLabelText("Account name"), "Test Account")); + await act(() => user.click(screen.getByRole("button", { name: "Continue" }))); + await act(() => user.type(screen.getByLabelText("Password"), "test-password")); + await act(() => user.click(screen.getByRole("button", { name: "Submit" }))); + + expect(mockDeriveMnemonicAccount).toHaveBeenCalledWith({ + fingerPrint: account.seedFingerPrint, + password: "test-password", + label: "Test Account", + }); + }); +}); diff --git a/apps/web/src/components/DeriveMnemonicAccountModal/DeriveMnemonicAccountModal.tsx b/apps/web/src/components/AccountSelectorModal/DeriveMnemonicAccountModal/DeriveMnemonicAccountModal.tsx similarity index 92% rename from apps/web/src/components/DeriveMnemonicAccountModal/DeriveMnemonicAccountModal.tsx rename to apps/web/src/components/AccountSelectorModal/DeriveMnemonicAccountModal/DeriveMnemonicAccountModal.tsx index 1352555b0d..1355b0d855 100644 --- a/apps/web/src/components/DeriveMnemonicAccountModal/DeriveMnemonicAccountModal.tsx +++ b/apps/web/src/components/AccountSelectorModal/DeriveMnemonicAccountModal/DeriveMnemonicAccountModal.tsx @@ -3,8 +3,8 @@ import { useDynamicModalContext } from "@umami/components"; import { DEFAULT_ACCOUNT_LABEL, type MnemonicAccount } from "@umami/core"; import { useAsyncActionHandler, useDeriveMnemonicAccount } from "@umami/state"; -import { MasterPasswordModal } from "../MasterPasswordModal"; -import { NameAccountModal } from "../NameAccountModal"; +import { MasterPasswordModal } from "../../MasterPasswordModal"; +import { NameAccountModal } from "../../NameAccountModal"; type DeriveMnemonicAccountModalProps = { account: MnemonicAccount; @@ -14,8 +14,8 @@ export const DeriveMnemonicAccountModal = ({ account }: DeriveMnemonicAccountMod const { goToIndex, openWith } = useDynamicModalContext(); const { handleAsyncAction } = useAsyncActionHandler(); - const toast = useToast(); const deriveMnemonicAccount = useDeriveMnemonicAccount(); + const toast = useToast(); const handleNameSubmit = ({ accountName }: { accountName: string }) => { const handlePasswordSubmit = ({ password }: { password: string }) => diff --git a/apps/web/src/components/DeriveMnemonicAccountModal/index.ts b/apps/web/src/components/AccountSelectorModal/DeriveMnemonicAccountModal/index.ts similarity index 100% rename from apps/web/src/components/DeriveMnemonicAccountModal/index.ts rename to apps/web/src/components/AccountSelectorModal/DeriveMnemonicAccountModal/index.ts diff --git a/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.test.tsx b/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.test.tsx new file mode 100644 index 0000000000..365114648d --- /dev/null +++ b/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.test.tsx @@ -0,0 +1,26 @@ +import { MasterPasswordModal } from "./MasterPasswordModal"; +import { renderInModal, screen, userEvent } from "../../testUtils"; + +const mockOnSubmit = jest.fn(); + +describe("", () => { + it("calls onSubmit with entered password when form is submitted", async () => { + const user = userEvent.setup(); + await renderInModal(); + + await user.type(screen.getByLabelText("Password"), "testpassword"); + await user.click(screen.getByRole("button", { name: "Submit" })); + + expect(mockOnSubmit).toHaveBeenCalledWith({ password: "testpassword" }); + }); + + it("shows validation error when submitting without a password", async () => { + const user = userEvent.setup(); + await renderInModal(); + + await user.click(screen.getByRole("button", { name: "Submit" })); + + expect(screen.getByText("Password is required")).toBeInTheDocument(); + expect(mockOnSubmit).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.tsx b/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.tsx index 7f7b94c17d..adb9ed3480 100644 --- a/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.tsx +++ b/apps/web/src/components/MasterPasswordModal/MasterPasswordModal.tsx @@ -32,7 +32,7 @@ export const MasterPasswordModal = ({ onSubmit }: MasterPasswordModalProps) => { }); return ( - + diff --git a/apps/web/src/components/NameAccountModal/NameAccountModal.test.tsx b/apps/web/src/components/NameAccountModal/NameAccountModal.test.tsx new file mode 100644 index 0000000000..8c4d88ce9c --- /dev/null +++ b/apps/web/src/components/NameAccountModal/NameAccountModal.test.tsx @@ -0,0 +1,28 @@ +import { NameAccountModal } from "./NameAccountModal"; +import { act, renderInModal, screen, userEvent, waitFor } from "../../testUtils"; + +const mockOnSubmit = jest.fn(); + +describe("NameAccountModal", () => { + it("renders with custom title and subtitle", async () => { + await renderInModal( + + ); + + await waitFor(() => { + expect(screen.getByText("Custom Title")).toBeVisible(); + }); + expect(screen.getByText("Custom Subtitle")).toBeVisible(); + }); + + it("calls onSubmit with form data when submitted", async () => { + const user = userEvent.setup(); + + await renderInModal(); + + await act(() => user.type(screen.getByLabelText("Account name"), "Test Account")); + await act(() => user.click(screen.getByRole("button", { name: "Continue" }))); + + expect(mockOnSubmit).toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/components/NameAccountModal/NameAccountModal.tsx b/apps/web/src/components/NameAccountModal/NameAccountModal.tsx index f9872680b6..c4e907803b 100644 --- a/apps/web/src/components/NameAccountModal/NameAccountModal.tsx +++ b/apps/web/src/components/NameAccountModal/NameAccountModal.tsx @@ -42,7 +42,7 @@ export const NameAccountModal = ({ const { register, handleSubmit } = form; return ( - + diff --git a/apps/web/src/components/Onboarding/SetupPassword/SetupPassword.test.tsx b/apps/web/src/components/Onboarding/SetupPassword/SetupPassword.test.tsx index fec3e538cb..c3dde1e3dc 100644 --- a/apps/web/src/components/Onboarding/SetupPassword/SetupPassword.test.tsx +++ b/apps/web/src/components/Onboarding/SetupPassword/SetupPassword.test.tsx @@ -130,7 +130,9 @@ describe("", () => { describe("mnemonic mode", () => { let store: UmamiStore; - const allFormValues = { mnemonic: mnemonic1.split(" ").map(word => ({ val: word })) }; + const allFormValues = { + current: { mnemonic: mnemonic1.split(" ").map(word => ({ val: word })) }, + }; const mockRestoreFromMnemonic = jest.fn(); beforeEach(() => { diff --git a/apps/web/src/testUtils.tsx b/apps/web/src/testUtils.tsx index d8303bbbde..fcf2467884 100644 --- a/apps/web/src/testUtils.tsx +++ b/apps/web/src/testUtils.tsx @@ -158,7 +158,9 @@ export const renderInModal = async ( if (allFormValues) { await act(() => - result.current.openWith() + result.current.openWith( + + ) ); fireEvent.click(screen.getByRole("button")); }