Skip to content

Commit

Permalink
Move network fields validation scheme to @umami/components pkg
Browse files Browse the repository at this point in the history
  • Loading branch information
OKendigelyan committed Oct 4, 2024
1 parent 60ae1cd commit 6f2fecb
Show file tree
Hide file tree
Showing 30 changed files with 168 additions and 292 deletions.
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
},
"packageManager": "[email protected]",
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"electron-updater": "6.3.4"
}
}
66 changes: 50 additions & 16 deletions apps/desktop/src/views/settings/network/UpsertNetworkModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ beforeEach(() => {
});

describe("<UpsertNetworkModal />", () => {
const updatedNetwork = {
...customNetwork,
rpcUrl: "https://rpc.com",
tzktApiUrl: "https://tzkt.com",
tzktExplorerUrl: "https://explorer.com",
buyTezUrl: "",
};

describe("edit mode", () => {
beforeEach(() => {
store.dispatch(networksActions.upsertNetwork(customNetwork));
Expand All @@ -27,14 +35,6 @@ describe("<UpsertNetworkModal />", () => {
const user = userEvent.setup();
render(<UpsertNetworkModal network={customNetwork} />, { store });

const updatedNetwork = {
...customNetwork,
rpcUrl: "https://rpc",
tzktApiUrl: "https://tzkt",
tzktExplorerUrl: "https://explorer",
buyTezUrl: "",
};

await act(() => user.clear(screen.getByLabelText("RPC URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt API URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt Explorer URL")));
Expand All @@ -55,14 +55,6 @@ describe("<UpsertNetworkModal />", () => {
const user = userEvent.setup();
render(<UpsertNetworkModal network={customNetwork} />, { store });

const updatedNetwork = {
...customNetwork,
rpcUrl: "https://rpc",
tzktApiUrl: "https://tzkt",
tzktExplorerUrl: "https://explorer",
buyTezUrl: "",
};

await act(() => user.clear(screen.getByLabelText("RPC URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt API URL")));
await act(() => user.clear(screen.getByLabelText("Tzkt Explorer URL")));
Expand All @@ -85,6 +77,48 @@ describe("<UpsertNetworkModal />", () => {
});
});

describe("URL fields validation", () => {
const urlFields = [
{ label: "RPC URL", required: true },
{ label: "Tzkt API URL", required: true },
{ label: "Tzkt Explorer URL", required: true },
{ label: "Buy Tez URL", required: false },
];

it.each(urlFields)("validates $label field", async ({ label, required }) => {
const user = userEvent.setup();
render(<UpsertNetworkModal />, { store });

await user.type(screen.getByLabelText(label), "invalid-url");
await user.tab();

await waitFor(() => {
expect(screen.getByText(`Enter a valid ${label}`)).toBeVisible();
});

await user.clear(screen.getByLabelText(label));
await user.tab();

if (required) {
await waitFor(() => {
expect(screen.getByText(`${label} is required`)).toBeVisible();
});
} else {
await waitFor(() => {
expect(screen.queryByText(`${label} is required`)).not.toBeInTheDocument();
});
}

await user.type(screen.getByLabelText(label), "https://valid-url.com");
await user.tab();

await waitFor(() => {
expect(screen.queryByText(`Enter a valid ${label}`)).not.toBeInTheDocument();
});
expect(screen.queryByText(`${label} is required`)).not.toBeInTheDocument();
});
});

describe("create mode", () => {
describe("name field", () => {
it("validates uniqueness", async () => {
Expand Down
22 changes: 10 additions & 12 deletions apps/desktop/src/views/settings/network/UpsertNetworkModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
ModalFooter,
ModalHeader,
} from "@chakra-ui/react";
import { useDynamicModalContext } from "@umami/components";
import { zodResolver } from "@hookform/resolvers/zod";
import { getNetworkValidationScheme, useDynamicModalContext } from "@umami/components";
import { networksActions, useAvailableNetworks } from "@umami/state";
import { type Network } from "@umami/tezos";
import { useForm } from "react-hook-form";
Expand All @@ -30,7 +31,11 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
formState: { errors, isValid },
register,
handleSubmit,
} = useForm<Network>({ mode: "onBlur", defaultValues: network });
} = useForm<Network>({
mode: "onBlur",
defaultValues: network,
resolver: zodResolver(getNetworkValidationScheme(availableNetworks, network)),
});

const onSubmit = (network: Network) => {
dispatch(networksActions.upsertNetwork(network));
Expand All @@ -51,12 +56,7 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
<Input
placeholder="mainnet"
{...register("name", {
required: "Name is required",
validate: name => {
if (availableNetworks.find(n => n.name === name)) {
return "Network with this name already exists";
}
},
setValueAs: removeTrailingSlashes,
})}
/>
{errors.name && <FormErrorMessage>{errors.name.message}</FormErrorMessage>}
Expand All @@ -67,7 +67,6 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
<Input
placeholder="https://prod.tcinfra.net/rpc/mainnet"
{...register("rpcUrl", {
required: "RPC URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -78,7 +77,6 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
<Input
placeholder="https://api.ghostnet.tzkt.io"
{...register("tzktApiUrl", {
required: "Tzkt API URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -89,7 +87,6 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
<Input
placeholder="https://ghostnet.tzkt.io"
{...register("tzktExplorerUrl", {
required: "Tzkt Explorer URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -98,9 +95,10 @@ export const UpsertNetworkModal = ({ network }: { network?: Network }) => {
)}
</FormControl>

<FormControl>
<FormControl isInvalid={!!errors.buyTezUrl}>
<FormLabel>Buy Tez URL</FormLabel>
<Input placeholder="https://faucet.ghostnet.teztnets.com" {...register("buyTezUrl")} />
{errors.buyTezUrl && <FormErrorMessage>{errors.buyTezUrl.message}</FormErrorMessage>}
</FormControl>
<ModalFooter>
<Button width="100%" isDisabled={!isValid} onClick={() => {}} type="submit">
Expand Down
87 changes: 42 additions & 45 deletions apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,48 @@ describe("<EditNetworkMenu />", () => {
});
});

describe("URL fields validation", () => {
const urlFields = [
{ label: "RPC URL", required: true },
{ label: "Tzkt API URL", required: true },
{ label: "Tzkt Explorer URL", required: true },
{ label: "Buy Tez URL", required: false },
];

it.each(urlFields)("validates $label field", async ({ label, required }) => {
const user = userEvent.setup();
await renderInDrawer(<EditNetworkMenu />, store);

await user.type(screen.getByLabelText(label), "invalid-url");
await user.tab();

await waitFor(() => {
expect(screen.getByText(`Enter a valid ${label}`)).toBeVisible();
});

await user.clear(screen.getByLabelText(label));
await user.tab();

if (required) {
await waitFor(() => {
expect(screen.getByText(`${label} is required`)).toBeVisible();
});
} else {
await waitFor(() => {
expect(screen.queryByText(`${label} is required`)).not.toBeInTheDocument();
});
}

await user.type(screen.getByLabelText(label), "https://valid-url.com");
await user.tab();

await waitFor(() => {
expect(screen.queryByText(`Enter a valid ${label}`)).not.toBeInTheDocument();
});
expect(screen.queryByText(`${label} is required`)).not.toBeInTheDocument();
});
});

describe("create mode", () => {
describe("name field", () => {
it("validates uniqueness", async () => {
Expand All @@ -99,51 +141,6 @@ describe("<EditNetworkMenu />", () => {
});
});

describe("URL fields validation", () => {
const urlFields = [
{ label: "RPC URL", required: true },
{ label: "Tzkt API URL", required: true },
{ label: "Tzkt Explorer URL", required: true },
{ label: "Buy Tez URL", required: false },
];

it.each(urlFields)("validates $label field", async ({ label, required }) => {
const user = userEvent.setup();
await renderInDrawer(<EditNetworkMenu />, store);

// Test invalid URL
await user.type(screen.getByLabelText(label), "invalid-url");
await user.tab(); // Trigger blur event

await waitFor(() => {
expect(screen.getByText(`Enter a valid ${label}`)).toBeVisible();
});

// Test empty field
await user.clear(screen.getByLabelText(label));
await user.tab(); // Trigger blur event

if (required) {
await waitFor(() => {
expect(screen.getByText(`${label} is required`)).toBeVisible();
});
} else {
await waitFor(() => {
expect(screen.queryByText(`${label} is required`)).not.toBeInTheDocument();
});
}

// Test valid URL
await user.type(screen.getByLabelText(label), "https://valid-url.com");
await user.tab(); // Trigger blur event

await waitFor(() => {
expect(screen.queryByText(`Enter a valid ${label}`)).not.toBeInTheDocument();
});
expect(screen.queryByText(`${label} is required`)).not.toBeInTheDocument();
});
});

it("validates RPC URL field presence", async () => {
await renderInDrawer(<EditNetworkMenu />, store);
fireEvent.blur(screen.getByLabelText("RPC URL"));
Expand Down
25 changes: 2 additions & 23 deletions apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Button, FormControl, FormErrorMessage, FormLabel, Input, VStack } from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useDynamicDrawerContext } from "@umami/components";
import { getNetworkValidationScheme, useDynamicDrawerContext } from "@umami/components";
import { networksActions, useAppDispatch, useAvailableNetworks } from "@umami/state";
import { type Network } from "@umami/tezos";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { DrawerContentWrapper } from "../DrawerContentWrapper";

Expand All @@ -14,25 +13,6 @@ type EditNetworkMenuProps = {

const removeTrailingSlashes = (url: string) => url.replace(/\/+$/g, "");

const getNetworkSchema = (availableNetworks: Network[], network?: Network) =>
z.object({
name: network
? z.string().optional()
: z
.string()
.min(1, "Name is required")
.refine(name => !availableNetworks.find(n => n.name === name), {
message: "Network with this name already exists",
}),
rpcUrl: z.string().min(1, "RPC URL is required").url("Enter a valid RPC URL"),
tzktApiUrl: z.string().min(1, "Tzkt API URL is required").url("Enter a valid Tzkt API URL"),
tzktExplorerUrl: z
.string()
.min(1, "Tzkt Explorer URL is required")
.url("Enter a valid Tzkt Explorer URL"),
buyTezUrl: z.string().url("Enter a valid Buy Tez URL").or(z.literal("")),
});

export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
const { goBack } = useDynamicDrawerContext();
const dispatch = useAppDispatch();
Expand All @@ -45,7 +25,7 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
} = useForm<Network>({
mode: "onBlur",
defaultValues: network,
resolver: zodResolver(getNetworkSchema(availableNetworks, network)),
resolver: zodResolver(getNetworkValidationScheme(availableNetworks, network)),
});

const onSubmit = (network: Network) => {
Expand Down Expand Up @@ -100,7 +80,6 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
<FormControl isInvalid={!!errors.buyTezUrl}>
<FormLabel>Buy Tez URL</FormLabel>
<Input placeholder="https://faucet.ghostnet.teztnets.com" {...register("buyTezUrl")} />

{errors.buyTezUrl && <FormErrorMessage>{errors.buyTezUrl.message}</FormErrorMessage>}
</FormControl>
</VStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { GHOSTNET, formatPkh, mockImplicitAddress } from "@umami/tezos";

import { AddressPillText } from "./AddressPillText";
import { mockFA2AddressKind } from "./testUtils";
import { render, screen } from "../testUtils";
import { render, screen } from "../../testUtils";
const { upsert } = contactsActions;

let store: UmamiStore;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { mockContractAddress } from "@umami/tezos";

import { type FA2Address } from "./types";
import { mockContractAddress } from "../../../tezos/src/testUtils";

export const mockFA2AddressKind = (index?: number): FA2Address => ({
type: "fa2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
import { cloneDeep } from "lodash";

import { useAddressKind } from "./useAddressKind";
import { renderHook } from "../testUtils";
import { renderHook } from "../../testUtils";

let store: UmamiStore;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { makeStore, UmamiStore } from "@umami/state";
import { type UmamiStore, makeStore } from "@umami/state";
import { mockImplicitAddress } from "@umami/tezos";

import { useAddressPill } from "./useAddressPill";
import { act, fireEvent, render, renderHook, screen, waitFor } from "../testUtils";
import { Box, useDisclosure } from "@chakra-ui/react";
import { act, renderHook } from "../../testUtils";

let store: UmamiStore;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useDynamicDrawer, useDynamicModal, useDynamicModalContext } from "./DynamicDisclosure";
import { act, render, renderHook, screen } from "../testUtils";
import { act, render, renderHook, screen } from "../../testUtils";

describe("DynamicDisclosure", () => {
describe("useDynamicModal", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {

import { useDynamicModalContext } from "./DynamicDisclosure";
import { useMultiForm } from "./useMultiForm";
import { act, renderHook, screen, userEvent, waitFor } from "../testUtils";
import { act, renderHook, screen, userEvent, waitFor } from "../../testUtils";

const Page3 = () => {
const { goBack, allFormValues } = useDynamicModalContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FormControl, Text } from "@chakra-ui/react";
import { FormProvider, useForm } from "react-hook-form";

import { MnemonicAutocomplete } from "./MnemonicAutocomplete";
import { fireEvent, render, screen, waitFor } from "../testUtils";
import { fireEvent, render, screen, waitFor } from "../../testUtils";

type FormFields = { word: string };

Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions packages/components/src/components/ReactIdenticon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ReactIdenticon";
Loading

0 comments on commit 6f2fecb

Please sign in to comment.