Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add url validation #2016

Merged
merged 5 commits into from
Oct 9, 2024
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
3 changes: 2 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@
"vite": "^5.4.8",
"vite-plugin-checker": "^0.8.0",
"vite-plugin-node-polyfills": "^0.17.0",
"zod": "^3.23.8"
"zod": "^3.23.8",
"@hookform/resolvers": "^3.9.0"
},
"packageManager": "[email protected]",
"dependencies": {
Expand Down
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
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@chakra-ui/theme-tools": "^2.2.3",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@hookform/resolvers": "^3.9.0",
"@reduxjs/toolkit": "^2.2.7",
"@tanstack/react-query": "^5.59.0",
"@taquito/beacon-wallet": "^20.0.1",
Expand Down
70 changes: 53 additions & 17 deletions apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ beforeEach(() => {

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

beforeEach(() => {
store.dispatch(networksActions.upsertNetwork(customNetwork));
});
Expand All @@ -27,14 +35,6 @@ describe("<EditNetworkMenu />", () => {
const user = userEvent.setup();
await renderInDrawer(<EditNetworkMenu 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 @@ -45,7 +45,9 @@ describe("<EditNetworkMenu />", () => {
user.type(screen.getByLabelText("Tzkt Explorer URL"), updatedNetwork.tzktExplorerUrl)
);

expect(screen.getByText("Save changes")).toBeEnabled();
await waitFor(() => {
expect(screen.getByText("Save changes")).toBeEnabled();
});

await act(() => user.click(screen.getByText("Save changes")));
expect(store.getState().networks.available).toEqual([MAINNET, GHOSTNET, updatedNetwork]);
Expand All @@ -55,14 +57,6 @@ describe("<EditNetworkMenu />", () => {
const user = userEvent.setup();
await renderInDrawer(<EditNetworkMenu 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 +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 Down
27 changes: 10 additions & 17 deletions apps/web/src/components/Menu/NetworkMenu/EditNetworkMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Button, FormControl, FormErrorMessage, FormLabel, Input, VStack } from "@chakra-ui/react";
import { useDynamicDrawerContext } from "@umami/components";
import { zodResolver } from "@hookform/resolvers/zod";
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";
Expand All @@ -21,7 +22,11 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
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 @@ -35,17 +40,7 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
{!network && (
<FormControl isInvalid={!!errors.name}>
<FormLabel>Name</FormLabel>
<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";
}
},
})}
/>
<Input placeholder="mainnet" {...register("name")} />
{errors.name && <FormErrorMessage>{errors.name.message}</FormErrorMessage>}
</FormControl>
)}
Expand All @@ -54,7 +49,6 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
<Input
placeholder="https://prod.tcinfra.net/rpc/mainnet"
{...register("rpcUrl", {
required: "RPC URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -65,7 +59,6 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
<Input
placeholder="https://api.ghostnet.tzkt.io"
{...register("tzktApiUrl", {
required: "Tzkt API URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -76,7 +69,6 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
<Input
placeholder="https://ghostnet.tzkt.io"
{...register("tzktExplorerUrl", {
required: "Tzkt Explorer URL is required",
setValueAs: removeTrailingSlashes,
})}
/>
Expand All @@ -85,9 +77,10 @@ export const EditNetworkMenu = ({ network }: EditNetworkMenuProps) => {
)}
</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>
</VStack>
<Button width="100%" marginTop="30px" isDisabled={!isValid} type="submit" variant="primary">
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
Loading
Loading