chore(dev): code sweep for tests and storybook

chore: updated README
chore: added button tests and stories
fix: removed unused code from @atoms/Currency
chore: added tests and stories for @atoms/Link
test: refactored NodeConnectSwitch refactored
chore: refactored NodeStatus
test: atom/ProtectedRoute
feat: added PasswordInput atom
chore: refactored account sidebar
test: PaymentMethodCard and AddPaymentMethod button
test: ReadyToUse molecule
test: SetPassword organism
test: Login page
chore: added stories for onboarding pages

---

Reviewed-by: localredhead
This commit is contained in:
Subir 2022-05-21 18:55:31 +05:30
parent 7ada7f17db
commit 8eb4694ca2
No known key found for this signature in database
GPG Key ID: 2D633D8047FD3FF0
86 changed files with 1948 additions and 226 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ node_modules
dist
*.local
thumbs.db
yarn-error.log
.eslintcache

View File

@ -3,5 +3,8 @@ node_modules
buildResources
packages/main/dist
packages/preload/dist
packages/rendered/dist
packages/renderer/dist
packages/main/coverage
packages/preload/coverage
packages/renderer/coverage
dist/

View File

@ -19,10 +19,7 @@ const svgrPlugin = require("vite-plugin-svgr");
const viteConfig = require("../packages/renderer/vite.config");
module.exports = {
stories: [
"../packages/renderer/**/*.stories.mdx",
"../packages/renderer/**/*.stories.@(js|jsx|ts|tsx)",
],
stories: ["../packages/renderer/src/**/*.stories.tsx"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",

View File

@ -1,3 +1,37 @@
# haveno-ui
# Haveno User Interface
Haveno user interface
## Development
### Prerequisites
1. Node 16.x
1. yarn 1.x
1. Haveno daemon and envoy proxy
### Install dependencies
```sh
yarn
```
### Configure environment variables
Copy [.env.example](./.env.example) to a file called `.env` and point the environment variables to the envoy proxy.
### Start the app in watch mode
```sh
yarn watch
```
### Tests
```sh
yarn tests
```
### Storybook
```sh
yarn storybook
```

View File

@ -46,10 +46,12 @@
"@storybook/addon-essentials": "^6.4.22",
"@storybook/addon-interactions": "^6.4.22",
"@storybook/addon-links": "^6.4.22",
"@storybook/builder-vite": "^0.1.29",
"@storybook/builder-vite": "^0.1.34",
"@storybook/react": "^6.4.22",
"@storybook/testing-library": "^0.0.10",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12",
"@testing-library/user-event": "^14.2.0",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.182",
"@types/react": "<18.0.0",
@ -69,6 +71,7 @@
"eslint-plugin-react": "^7.29.4",
"happy-dom": "2.41.0",
"husky": "^7.0.4",
"jsdom": "^19.0.0",
"nano-staged": "^0.7.0",
"playwright": "1.19.1",
"prettier": "^2.6.2",
@ -97,6 +100,7 @@
"react-intl": "^5.24.8",
"react-query": "^3.34.19",
"react-router-dom": "6",
"recoil": "^0.7.0"
"recoil": "^0.7.0",
"tabler-icons-react": "^1.48.0"
}
}

View File

@ -16,7 +16,7 @@
import { Group, Stack } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Button } from ".";
import { Button, TextButton } from ".";
export default {
title: "atoms/Buttons",
@ -30,12 +30,14 @@ const Template: ComponentStory<typeof Button> = () => {
<Button flavor="neutral">Neutral</Button>
<Button flavor="success">Success</Button>
<Button flavor="danger">Error</Button>
<TextButton>Click me</TextButton>
<Group>
<Button flavor="primary">Primary</Button>
<Button flavor="neutral">Neutral</Button>
<Button flavor="success">Success</Button>
<Button flavor="danger">Error</Button>
<TextButton>Click me</TextButton>
</Group>
</Stack>
);

View File

@ -16,7 +16,7 @@
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import { Button } from ".";
import { Button, TextButton } from ".";
describe("atoms::Buttons", () => {
it("renders primary button by default", () => {
@ -38,4 +38,11 @@ describe("atoms::Buttons", () => {
const { asFragment } = render(<Button flavor="danger">Error</Button>);
expect(asFragment()).toMatchSnapshot();
});
it("renders text button", () => {
const { asFragment } = render(
<TextButton>This is a Text Button</TextButton>
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -26,7 +26,7 @@ interface TextButtonProps extends UnstyledButtonProps<"button"> {
export function TextButton(props: TextButtonProps) {
const { children, ...rest } = props;
return (
<UnstyledButton {...rest}>
<UnstyledButton {...rest} sx={{ textAlign: "center" }}>
<BodyText component="span" heavy sx={{ textDecoration: "underline" }}>
{children}
</BodyText>

View File

@ -75,3 +75,18 @@ exports[`atoms::Buttons > renders success button 1`] = `
</button>
</DocumentFragment>
`;
exports[`atoms::Buttons > renders text button 1`] = `
<DocumentFragment>
<button
class="mantine-UnstyledButton-root mantine-13uxwbm"
type="button"
>
<span
class="mantine-Text-root mantine-10a0kig"
>
This is a Text Button
</span>
</button>
</DocumentFragment>
`;

View File

@ -28,8 +28,7 @@ export function Currency(props: CurrencyProps) {
const formattedNumber = useMemo(
() =>
intl
.formatNumber(value, {
intl.formatNumber(value, {
...(currencyCode
? {
currency: currencyCode,
@ -41,8 +40,7 @@ export function Currency(props: CurrencyProps) {
minimumFractionDigits: 2,
maximumFractionDigits: 12,
}),
})
.replace(/XXX\s/, ""),
}),
[currencyCode, value]
);

View File

@ -14,27 +14,26 @@
// limitations under the License.
// =============================================================================
import { useNavigate } from "react-router-dom";
import { SecondarySidebarItem } from "@molecules/SecondarySidebar";
import { useNavLinkActive } from "@src/hooks/misc/useNavLinkActive";
import { Stack } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import type { LinkProps } from ".";
import { Link } from ".";
interface AccountSidebarItemProps {
label: string;
route: string;
}
export function AccountSidebarItem({ label, route }: AccountSidebarItemProps) {
const isActive = useNavLinkActive({ to: route });
const navigate = useNavigate();
export default {
title: "atoms/Link",
component: Link,
} as ComponentMeta<typeof Link>;
const Template: ComponentStory<typeof Link> = ({ children, to }: LinkProps) => {
return (
<SecondarySidebarItem
key={label}
label={label}
isActive={isActive}
onClick={() => {
return navigate(route);
}}
/>
<Stack>
<Link to={to}>{children}</Link>
</Stack>
);
}
};
export const Default = Template.bind({});
Default.args = {
children: "Click me",
to: "/",
};

View File

@ -0,0 +1,31 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders";
import { Link } from ".";
describe("atoms::Link", () => {
it("renders without exploding", () => {
const { asFragment } = render(
<AppProviders>
<Link to="/">Click me</Link>
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -19,7 +19,7 @@ import { Link as RouterLink } from "react-router-dom";
import { BodyText } from "@atoms/Typography";
import type { ReactText } from "react";
interface LinkProps extends RouterLinkProps {
export interface LinkProps extends RouterLinkProps {
children: ReactText;
}

View File

@ -0,0 +1,15 @@
// Vitest Snapshot v1
exports[`atoms::Link > renders without exploding 1`] = `
<DocumentFragment>
<a
href="#/"
>
<span
class="mantine-Text-root mantine-ix3vgq"
>
Click me
</span>
</a>
</DocumentFragment>
`;

View File

@ -14,12 +14,13 @@
// limitations under the License.
// =============================================================================
import { Box, createStyles, Text } from "@mantine/core";
import { createStyles, Box, Text } from "@mantine/core";
export enum NodeStatusType {
Active = "active",
Inactive = "inactive",
}
export interface NodeStatusProps {
/** Node title */
title: string;
@ -34,9 +35,9 @@ export function NodeStatus({ title, status }: NodeStatusProps) {
return (
<Box className={classes.root}>
<Text className={classes.title}>{title}</Text>
<div className={classes.status}>
<div className={classes.statusInner} />
</div>
<Box className={classes.status}>
<Box className={classes.statusInner} />
</Box>
</Box>
);
}
@ -49,7 +50,7 @@ export const useStyles = createStyles<string, { status: NodeStatusType }>(
theme.colorScheme === "dark" ? theme.colors.dark[8] : theme.white,
border: `1px solid ${theme.colors.gray[2]}`,
borderRadius: theme.radius.md,
padding: "0.91rem",
padding: "0.875rem",
display: "flex",
transition: "background-color 0.1s ease-in-out",
},

View File

@ -3,7 +3,7 @@
exports[`atoms::NodeStatus > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-18xx7au"
class="mantine-1vax8o0"
>
<div
class="mantine-Text-root mantine-14byb36"
@ -11,15 +11,15 @@ exports[`atoms::NodeStatus > renders without exploding 1`] = `
node.moneroworldcom:18089:active
</div>
<div
class="mantine-17do188"
class="mantine-1d0mff5"
>
<div
class="mantine-167633s"
class="mantine-zy87za"
/>
</div>
</div>
<div
class="mantine-18xx7au"
class="mantine-1vax8o0"
>
<div
class="mantine-Text-root mantine-14byb36"
@ -27,10 +27,10 @@ exports[`atoms::NodeStatus > renders without exploding 1`] = `
node.moneroworldcom:18089:inactive
</div>
<div
class="mantine-17do188"
class="mantine-1d0mff5"
>
<div
class="mantine-1cipvbv"
class="mantine-19v6ci5"
/>
</div>
</div>

View File

@ -0,0 +1,43 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Stack } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { EyeCheck, EyeOff } from "tabler-icons-react";
import { PasswordInput } from ".";
export default {
title: "atoms/PasswordInput",
component: PasswordInput,
} as ComponentMeta<typeof PasswordInput>;
const Template: ComponentStory<typeof PasswordInput> = () => {
return (
<Stack>
<PasswordInput
id="password"
label="Change visibility toggle icon"
placeholder="Change visibility toggle icon"
defaultValue="secret"
visibilityToggleIcon={({ reveal, size }) =>
reveal ? <EyeOff size={size} /> : <EyeCheck size={size} />
}
/>
</Stack>
);
};
export const Default = Template.bind({});

View File

@ -0,0 +1,28 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import { PasswordInput } from ".";
describe("atoms::PasswordInput", () => {
it("renders without exploding", () => {
const { asFragment } = render(
<PasswordInput id="pass" label="Enter password" />
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,45 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { PasswordInputProps as MPasswordInputProps } from "@mantine/core";
import { createStyles, PasswordInput as MPasswordInput } from "@mantine/core";
interface PasswordInputProps extends MPasswordInputProps {
id: string;
}
export function PasswordInput(props: PasswordInputProps) {
const { id, ...rest } = props;
const { classes } = useStyles();
return <MPasswordInput classNames={classes} id={id} {...rest} />;
}
const useStyles = createStyles((theme) => ({
label: {
fontSize: "0.875rem",
fontWeight: 600,
marginBottom: theme.spacing.sm,
},
innerInput: {
fontSize: "0.875rem",
fontWeight: 700,
height: "3rem",
padding: "1rem",
},
input: {
height: "3rem",
},
}));

View File

@ -0,0 +1,56 @@
// Vitest Snapshot v1
exports[`atoms::PasswordInput > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-PasswordInput-root mantine-18udhi"
>
<label
class="mantine-PasswordInput-label mantine-7802ha"
for="pass"
id="pass-label"
>
Enter password
</label>
<div
class="mantine-PasswordInput-wrapper mantine-12sbrde"
>
<div
aria-invalid="false"
class="mantine-PasswordInput-defaultVariant mantine-PasswordInput-input mantine-PasswordInput-input mantine-1i2duzg"
>
<input
class="mantine-PasswordInput-innerInput mantine-1bj8gkk"
id="pass"
type="password"
/>
</div>
<div
class="mantine-o3oqoy mantine-PasswordInput-rightSection"
>
<button
aria-hidden="true"
class="mantine-ActionIcon-hover mantine-ActionIcon-root mantine-PasswordInput-visibilityToggle mantine-vao037"
tabindex="-1"
type="button"
>
<svg
fill="none"
height="15"
viewBox="0 0 15 15"
width="15"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
fill="currentColor"
fill-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -14,4 +14,4 @@
// limitations under the License.
// =============================================================================
export * from "./Wallet";
export * from "./PasswordInput";

View File

@ -0,0 +1,105 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
const navigateSpy = vi.fn();
const deleteSessionSpy = vi.fn();
import type { SpyInstanceFn } from "vitest";
import { beforeAll, describe, expect, it, vi, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { useAuth } from "@hooks/session/useAuth";
import { ProtectedRoute } from "./ProtectedRoute";
import { ROUTES } from "@constants/routes";
describe("atoms::ProtectedRoute", () => {
beforeAll(() => {
vi.mock("react-router-dom", () => ({
useNavigate: () => navigateSpy,
}));
vi.mock("@utils/session", () => ({
deleteSession: deleteSessionSpy,
}));
vi.mock("@hooks/session/useAuth", () => ({
useAuth: vi.fn(() => ({
isLoading: true,
isSuccess: false,
data: undefined,
})),
}));
});
afterEach(() => {
vi.resetAllMocks();
});
it("renders children if auth session exists", () => {
(useAuth as SpyInstanceFn).mockReturnValue({
isLoading: false,
isSuccess: true,
data: true,
});
const { asFragment, unmount } = render(
<ProtectedRoute>
<div>Protected content</div>
</ProtectedRoute>
);
expect(screen.queryByText("Protected content")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("skips rendering children and redirects to login if no auth session exists", () => {
(useAuth as SpyInstanceFn).mockReturnValueOnce({
isLoading: false,
isSuccess: true,
data: false,
});
const { asFragment, unmount } = render(
<ProtectedRoute>
<div>Protected content</div>
</ProtectedRoute>
);
expect(screen.queryByText("Protected content")).toBeNull();
expect(deleteSessionSpy).toHaveBeenCalledOnce();
expect(navigateSpy).toHaveBeenCalledOnce();
expect(navigateSpy).toHaveBeenCalledWith(ROUTES.Login);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("skips rendering children and redirects to login if auth session can't be retrieved", () => {
(useAuth as SpyInstanceFn).mockReturnValueOnce({
isLoading: false,
isSuccess: false,
data: true,
});
const { asFragment, unmount } = render(
<ProtectedRoute>
<div>Protected content</div>
</ProtectedRoute>
);
expect(screen.queryByText("Protected content")).toBeNull();
expect(deleteSessionSpy).toHaveBeenCalledOnce();
expect(navigateSpy).toHaveBeenCalledOnce();
expect(navigateSpy).toHaveBeenCalledWith(ROUTES.Login);
expect(asFragment()).toMatchSnapshot();
unmount();
});
});

View File

@ -18,7 +18,7 @@ import type { ReactNode } from "react";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "@hooks/session/useAuth";
import { deleteSession } from "@src/utils/session";
import { deleteSession } from "@utils/session";
import { ROUTES } from "@constants/routes";
export function ProtectedRoute({ children }: { children: ReactNode }) {
@ -29,11 +29,11 @@ export function ProtectedRoute({ children }: { children: ReactNode }) {
if (isLoading) {
return;
}
if (!isAuthed) {
if (!isAuthed || !isSuccess) {
deleteSession();
navigate(ROUTES.Login);
}
}, [isLoading, isAuthed]);
}, [isLoading, isAuthed, isSuccess]);
return isSuccess ? <>{children}</> : null;
return isSuccess && isAuthed ? <>{children}</> : null;
}

View File

@ -0,0 +1,13 @@
// Vitest Snapshot v1
exports[`atoms::ProtectedRoute > renders children if auth session exists 1`] = `
<DocumentFragment>
<div>
Protected content
</div>
</DocumentFragment>
`;
exports[`atoms::ProtectedRoute > skips rendering children and redirects to login if auth session can't be retrieved 1`] = `<DocumentFragment />`;
exports[`atoms::ProtectedRoute > skips rendering children and redirects to login if no auth session exists 1`] = `<DocumentFragment />`;

View File

@ -21,8 +21,8 @@ exports[`atoms::Select > renders without exploding 1`] = `
tabindex="-1"
>
<input
defaultvalue=""
type="hidden"
value=""
/>
<div
class="mantine-Select-wrapper mantine-12sbrde"
@ -33,11 +33,11 @@ exports[`atoms::Select > renders without exploding 1`] = `
autocomplete="nope"
class="mantine-Select-defaultVariant mantine-Select-input mantine-93d3e4"
data-mantine-stop-propagation="false"
defaultvalue=""
id="select"
placeholder="Pick one"
readonly=""
type="text"
value=""
/>
<div
class="mantine-Select-rightSection mantine-14dm59e"

View File

@ -16,6 +16,7 @@
import { Stack } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { ReactComponent as BtcIcon } from "@assets/btc.svg";
import { TextInput } from ".";
export default {
@ -23,17 +24,13 @@ export default {
component: TextInput,
} as ComponentMeta<typeof TextInput>;
const Template: ComponentStory<typeof TextInput> = (args) => {
const Template: ComponentStory<typeof TextInput> = () => {
return (
<Stack>
<TextInput {...args} />
<TextInput id="name" placeholder="Your name" label="Full name" required />
<TextInput id="btc" label="Bitcoin" icon={<BtcIcon />} />
</Stack>
);
};
export const Default = Template.bind({});
Default.args = {
id: "email",
label: "Your email",
placeholder: "johndoe@gmail.com",
};

View File

@ -15,12 +15,13 @@
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { NodeConnectSwitch } from ".";
import { Text } from "@mantine/core";
import { ReactComponent as CloudIcon } from "@assets/setting-cloud.svg";
import { ReactComponent as ServerIcon } from "@assets/setting-server.svg";
import { NodeConnectSwitch } from ".";
export default {
title: "atoms/NodeConnectSwitch",
title: "molecules/NodeConnectSwitch",
component: NodeConnectSwitch,
} as ComponentMeta<typeof NodeConnectSwitch>;
@ -28,13 +29,11 @@ const Template: ComponentStory<typeof NodeConnectSwitch> = () => {
return (
<NodeConnectSwitch>
<NodeConnectSwitch.Method
active={true}
current={true}
tabKey="local-node"
label="Local Node"
icon={<ServerIcon width="32px" height="62px" />}
>
Local Node
<Text>Local Node</Text>
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
@ -42,7 +41,7 @@ const Template: ComponentStory<typeof NodeConnectSwitch> = () => {
label="Remote Node"
icon={<CloudIcon width="58px" height="54px" />}
>
Remote Node
<Text>Remote Node</Text>
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
);

View File

@ -0,0 +1,112 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders";
import { ReactComponent as CloudIcon } from "@assets/setting-cloud.svg";
import { ReactComponent as ServerIcon } from "@assets/setting-server.svg";
import { NodeConnectSwitch } from ".";
describe("molecules::NodeConnectSwitch", () => {
it("renders without exploding", () => {
const { asFragment, unmount } = render(
<AppProviders>
<NodeConnectSwitch>
<NodeConnectSwitch.Method
active={true}
current={true}
tabKey="local-node"
label="Local Node"
icon={<ServerIcon width="32px" height="62px" />}
>
Local Node Content
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
tabKey="remote-node"
label="Remote Node"
icon={<CloudIcon width="58px" height="54px" />}
active={false}
current={false}
>
Remote Node Content
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("pre-renders active tab", () => {
const { unmount } = render(
<AppProviders>
<NodeConnectSwitch active="local-node">
<NodeConnectSwitch.Method
current
tabKey="local-node"
label="Local Node"
icon={<ServerIcon width="32px" height="62px" />}
>
Local Node Content
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
tabKey="remote-node"
label="Remote Node"
icon={<CloudIcon width="58px" height="54px" />}
>
Remote Node Content
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
</AppProviders>
);
expect(screen.queryByText("Local Node Content")).toBeInTheDocument();
unmount();
});
it("clicking a tab reveals its contents", () => {
const { unmount } = render(
<AppProviders>
<NodeConnectSwitch>
<NodeConnectSwitch.Method
current
tabKey="local-node"
label="Local Node"
icon={<ServerIcon width="32px" height="62px" />}
>
Local Node Content
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
tabKey="remote-node"
label="Remote Node"
icon={<CloudIcon width="58px" height="54px" />}
>
Remote Node Content
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
</AppProviders>
);
const tabs = screen.queryAllByRole("tab");
expect(tabs).toHaveLength(2);
expect(screen.queryByText("Remote Node Content")).not.toBeInTheDocument();
fireEvent.click(tabs[1]);
expect(screen.queryByText("Remote Node Content")).toBeInTheDocument();
unmount();
});
});

View File

@ -73,12 +73,12 @@ export function NodeConnectSwitch({
return (
<Box className={cx(classes.root, className)}>
<div className={cx(classes.tabsListWrapper)}>{panes} </div>
<Box className={cx(classes.tabsListWrapper)}>{panes}</Box>
{content && (
<div role="tabpanel" className={classes.body} key={_activeTab}>
<Box role="tabpanel" className={classes.body} key={_activeTab}>
{content.props.children}
</div>
</Box>
)}
</Box>
);

View File

@ -1,12 +1,12 @@
// Vitest Snapshot v1
exports[`atoms::NodeConnectSwitch > renders without exploding 1`] = `
exports[`molecules::NodeConnectSwitch > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-15po0m8"
>
<div
class="mantine-17do188"
class="mantine-1d0mff5"
>
<button
class="mantine-1lhe3fe"
@ -114,7 +114,6 @@ exports[`atoms::NodeConnectSwitch > renders without exploding 1`] = `
</div>
</div>
</button>
</div>
</div>
</DocumentFragment>

View File

@ -0,0 +1,39 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { AddPaymentMethodButton } from ".";
describe("molecules::AddPaymentMethodButton", () => {
it("renders without exploding", () => {
const spy = vi.fn();
const { asFragment, unmount } = render(
<AddPaymentMethodButton onClick={spy} />
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("calls onClick", () => {
const spy = vi.fn();
const { unmount } = render(<AddPaymentMethodButton onClick={spy} />);
expect(spy).to.not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button"));
expect(spy).to.toHaveBeenCalledTimes(1);
unmount();
});
});

View File

@ -0,0 +1,56 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Stack } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import {
CryptoCurrencyAccountPayload,
PaymentAccount,
PaymentAccountPayload,
} from "haveno-ts";
import { AddPaymentMethodButton, PaymentMethodCard } from ".";
export default {
title: "molecules/PaymentMethodCard",
component: PaymentMethodCard,
} as ComponentMeta<typeof PaymentMethodCard>;
const Template: ComponentStory<typeof PaymentMethodCard> = () => {
return (
<Stack>
<PaymentMethodCard data={paymentAccount1} />
</Stack>
);
};
const Template2: ComponentStory<typeof PaymentMethodCard> = () => {
return (
<Stack>
<AddPaymentMethodButton onClick={() => console.log("onClick called")} />
</Stack>
);
};
export const Default = Template.bind({});
export const AddButton = Template2.bind({});
const paymentAccount1 = new PaymentAccount();
paymentAccount1.setAccountName("BTC Account 1");
const cryptoAccPayload1 = new CryptoCurrencyAccountPayload();
cryptoAccPayload1.setAddress("01234567abcdef");
const paymentAccPayload1 = new PaymentAccountPayload();
paymentAccPayload1.setCryptoCurrencyAccountPayload(cryptoAccPayload1);
paymentAccount1.setPaymentAccountPayload(paymentAccPayload1);

View File

@ -16,38 +16,30 @@
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import {
CryptoCurrencyAccountPayload,
PaymentAccount,
PaymentAccountPayload,
} from "haveno-ts";
import { AppProviders } from "@atoms/AppProviders";
import { ReactComponent as CloudIcon } from "@assets/setting-cloud.svg";
import { ReactComponent as ServerIcon } from "@assets/setting-server.svg";
import { NodeConnectSwitch } from ".";
import { PaymentMethodCard } from ".";
describe("atoms::NodeConnectSwitch", () => {
describe("molecules::PaymentMethodCard", () => {
it("renders without exploding", () => {
const { asFragment } = render(
const { asFragment, unmount } = render(
<AppProviders>
<NodeConnectSwitch>
<NodeConnectSwitch.Method
active={true}
current={true}
tabKey={"local-node"}
label="Local Node"
icon={<ServerIcon width="32px" height="62px" />}
>
Local Node
</NodeConnectSwitch.Method>
<NodeConnectSwitch.Method
tabKey={"remote-node"}
label="Remote Node"
icon={<CloudIcon width="58px" height="54px" />}
active={false}
current={false}
>
Remote Node
</NodeConnectSwitch.Method>
</NodeConnectSwitch>
<PaymentMethodCard data={paymentAccount1} />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
});
const paymentAccount1 = new PaymentAccount();
paymentAccount1.setAccountName("BTC Account 1");
const cryptoAccPayload1 = new CryptoCurrencyAccountPayload();
cryptoAccPayload1.setAddress("01234567abcdef");
const paymentAccPayload1 = new PaymentAccountPayload();
paymentAccPayload1.setCryptoCurrencyAccountPayload(cryptoAccPayload1);
paymentAccount1.setPaymentAccountPayload(paymentAccPayload1);

View File

@ -31,7 +31,7 @@ import {
getPaymentAccountLogo,
getPaymentAccountName,
getPaymentAccountNumber,
} from "@src/utils/payment-account";
} from "@utils/payment-account";
interface PaymentMethodCardProps {
data: PaymentAccount;

View File

@ -0,0 +1,24 @@
// Vitest Snapshot v1
exports[`molecules::AddPaymentMethodButton > renders without exploding 1`] = `
<DocumentFragment>
<button
class="mantine-UnstyledButton-root mantine-17b3lsu"
type="button"
>
<svg
fill="none"
height="1em"
viewBox="0 0 28 28"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14 0C6.3 0 0 6.3 0 14C0 21.7 6.3 28 14 28C21.7 28 28 21.7 28 14C28 6.3 21.7 0 14 0ZM21 15H15V21C15 21.55 14.55 22 14 22C13.45 22 13 21.55 13 21V15H7C6.45 15 6 14.55 6 14C6 13.45 6.45 13 7 13C7 13 10.05 13 13 13V7C13 6.45 13.45 6 14 6C14.55 6 15 6.45 15 7V13C17.95 13 21 13 21 13C21.55 13 22 13.45 22 14C22 14.55 21.55 15 21 15Z"
fill="#0B65DA"
fill-opacity="0.25"
/>
</svg>
</button>
</DocumentFragment>
`;

View File

@ -0,0 +1,67 @@
// Vitest Snapshot v1
exports[`molecules::PaymentMethodCard > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-1lb8n9h"
>
<div
class="mantine-Stack-root mantine-lfk3cq"
>
<div
class="mantine-Group-root mantine-19jxmdp"
>
<div
class="mantine-Group-root mantine-Group-child mantine-zah578"
>
<svg
class="mantine-gnzaph mantine-Group-child"
fill="none"
height="1em"
viewBox="0 0 28 28"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M27.578 17.3866C25.7081 24.8866 18.1118 29.4511 10.6109 27.5808C3.11301 25.7109 -1.45142 18.1141 0.419327 10.6145C2.28833 3.11358 9.88464 -1.45129 17.3834 0.418582C24.8839 2.28846 29.4479 9.88608 27.578 17.3866Z"
fill="#F7931A"
/>
<path
d="M20.173 12.0055C20.4516 10.1426 19.0333 9.14117 17.0938 8.47311L17.723 5.94961L16.1869 5.5668L15.5744 8.0238C15.1706 7.92317 14.7558 7.82824 14.3437 7.73417L14.9606 5.26099L13.4254 4.87817L12.7958 7.4008C12.4616 7.32467 12.1335 7.24942 11.815 7.17024L11.8167 7.16236L9.69833 6.63342L9.28971 8.27405C9.28971 8.27405 10.4294 8.53524 10.4053 8.55142C11.0275 8.70674 11.1399 9.11842 11.1211 9.4448L10.4045 12.3196C10.4473 12.3305 10.5029 12.3463 10.5641 12.3708C10.513 12.3581 10.4583 12.3441 10.4018 12.3305L9.39733 16.3577C9.32121 16.5467 9.12827 16.8302 8.69339 16.7226C8.70871 16.7449 7.57689 16.4439 7.57689 16.4439L6.81433 18.2022L8.81327 18.7005C9.18514 18.7937 9.54958 18.8913 9.90833 18.9832L9.27264 21.5355L10.807 21.9184L11.4365 19.3931C11.8556 19.5069 12.2625 19.6119 12.6606 19.7107L12.0333 22.2242L13.5693 22.607L14.205 20.0594C16.8243 20.5551 18.794 20.3552 19.623 17.9861C20.2911 16.0786 19.5898 14.9783 18.2116 14.2608C19.2153 14.0294 19.9713 13.3692 20.173 12.0055ZM16.6633 16.9269C16.1886 18.8344 12.977 17.8032 11.9357 17.5447L12.7792 14.1632C13.8205 14.4231 17.1595 14.9376 16.6633 16.9269ZM17.1385 11.9779C16.7053 13.713 14.0322 12.8315 13.1651 12.6154L13.9298 9.54849C14.797 9.76461 17.5895 10.168 17.1385 11.9779Z"
fill="white"
/>
</svg>
<div
class="mantine-Text-root mantine-Group-child mantine-xrxbf2"
>
BTC
</div>
</div>
<button
class="mantine-UnstyledButton-root mantine-Group-child mantine-1e6tu8r"
type="button"
>
<svg
class="mantine-ezordu"
fill="none"
height="1em"
viewBox="0 0 15 4"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.375 1.875C9.375 2.925 8.5125 3.75 7.5 3.75C6.4875 3.75 5.625 2.8875 5.625 1.875C5.625 0.8625 6.4875 0 7.5 0C8.5125 0 9.375 0.8625 9.375 1.875ZM13.125 0C12.075 0 11.25 0.8625 11.25 1.875C11.25 2.8875 12.075 3.75 13.125 3.75C14.175 3.75 15 2.8875 15 1.875C15 0.8625 14.175 0 13.125 0ZM1.875 0C0.825 0 0 0.8625 0 1.875C0 2.8875 0.8625 3.75 1.875 3.75C2.8875 3.75 3.75 2.8875 3.75 1.875C3.75 0.8625 2.925 0 1.875 0Z"
fill="#111111"
/>
</svg>
</button>
</div>
<div
class="mantine-Text-root mantine-17kke86"
>
01234567abcdef
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -14,4 +14,5 @@
// limitations under the License.
// =============================================================================
// TODO @subir: move this to @constants/currencies
export type SupportedCurrencies = "BTC" | "ETH" | "EUR";

View File

@ -0,0 +1,34 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { Container } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { ReadyToUse } from ".";
export default {
title: "molecules/ReadyToUse",
component: ReadyToUse,
} as ComponentMeta<typeof ReadyToUse>;
const Template: ComponentStory<typeof ReadyToUse> = () => {
return (
<Container>
<ReadyToUse onSubmit={() => console.log("onSubmit called")} />
</Container>
);
};
export const Default = Template.bind({});

View File

@ -0,0 +1,37 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { ReadyToUse } from ".";
describe("molecules::ReadyToUse", () => {
it("renders without exploding", () => {
const spy = vi.fn();
const { asFragment, unmount } = render(<ReadyToUse onSubmit={spy} />);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("calls onSubmit", () => {
const spy = vi.fn();
const { unmount } = render(<ReadyToUse onSubmit={spy} />);
expect(spy).to.not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button"));
expect(spy).to.toHaveBeenCalledTimes(1);
unmount();
});
});

View File

@ -0,0 +1,41 @@
// Vitest Snapshot v1
exports[`molecules::ReadyToUse > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-Stack-root mantine-lfk3cq"
>
<h1
class="mantine-Title-root mantine-xz2ygd"
>
Haveno is ready for use.
</h1>
<div
class="mantine-Text-root mantine-1w4ewzw"
>
Youve succesfully set up Haveno. Please note that to be able to trade, you need to deposit Monero in your Haveno wallet and set up a payment account.
</div>
<div
class="mantine-63n06h"
/>
<div
class="mantine-Group-root mantine-19jxmdp"
>
<button
class="mantine-Button-filled mantine-Button-root mantine-Group-child mantine-1bqp2m7"
type="submit"
>
<div
class="mantine-3xbgk5 mantine-Button-inner"
>
<span
class="mantine-qo1k2 mantine-Button-label"
>
Start using Haveno
</span>
</div>
</button>
</div>
</div>
</DocumentFragment>
`;

View File

@ -16,19 +16,19 @@
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders";
import { SecondarySidebar, SecondarySidebarItem } from "./";
import { ThemeProvider } from "@atoms/AppProviders/ThemeProvider";
describe("molecules::SecondarySidebar", () => {
it("renders without exploding", () => {
const { asFragment } = render(
<ThemeProvider>
<AppProviders>
<SecondarySidebar>
<SecondarySidebarItem label="Active item" isActive={true} />
<SecondarySidebarItem label="Inactive item" isActive={false} />
<SecondarySidebarItem label="Active item" isActive={true} />
</SecondarySidebar>
</ThemeProvider>
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
});

View File

@ -25,7 +25,9 @@ interface SecondarySidebarProps {
* @param {SecondarySidebarProps}
* @returns {JSX.Element}
*/
export function SecondarySidebar({ children }: SecondarySidebarProps) {
export function SecondarySidebar({
children,
}: SecondarySidebarProps): JSX.Element {
const { classes } = useStyles();
return (

View File

@ -14,12 +14,14 @@
// limitations under the License.
// =============================================================================
import { useNavLinkActive } from "@hooks/misc/useNavLinkActive";
import { UnstyledButton, Group, Text, createStyles } from "@mantine/core";
interface SecondarySidebarItemProps {
isActive?: boolean;
label: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
route?: string;
}
/**
@ -31,8 +33,10 @@ export function SecondarySidebarItem({
isActive = false,
label,
onClick,
}: SecondarySidebarItemProps) {
const { classes } = useStyles({ isActive });
route,
}: SecondarySidebarItemProps): JSX.Element {
const isRouteActive = useNavLinkActive({ to: route });
const { classes } = useStyles({ isActive: isRouteActive || isActive });
return (
<UnstyledButton className={classes.button} onClick={onClick}>

View File

@ -18,7 +18,7 @@ import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { AccountSidebar } from "./AccountSidebar";
export default {
title: "molecules/AccountSidebar",
title: "organisms/AccountSidebar",
component: AccountSidebar,
} as ComponentMeta<typeof AccountSidebar>;

View File

@ -16,7 +16,6 @@
import { describe, expect, it } from "vitest";
import { render } from "@testing-library/react";
import { Routes, Route } from "react-router-dom";
import { AppProviders } from "@atoms/AppProviders";
import { AccountSidebar } from "./AccountSidebar";
@ -24,9 +23,7 @@ describe("molecules::AccountSidebar", () => {
it("renders without exploding", () => {
const { asFragment } = render(
<AppProviders>
<Routes>
<Route path="/" element={<AccountSidebar />} />
</Routes>
<AccountSidebar />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();

View File

@ -16,15 +16,18 @@
import { createStyles, Box, Title } from "@mantine/core";
import { FormattedMessage } from "react-intl";
import { SecondarySidebar } from "@molecules/SecondarySidebar";
import { useNavigate } from "react-router-dom";
import {
SecondarySidebar,
SecondarySidebarItem,
} from "@molecules/SecondarySidebar";
import { LangKeys } from "@constants/lang";
import { WIDTH } from "./_constants";
import { useGetAccountSidebarMenu } from "./_hooks";
import { AccountSidebarItem } from "./AccountSidebarItem";
export function AccountSidebar() {
const { classes } = useStyles();
const navigate = useNavigate();
const menu = useGetAccountSidebarMenu();
return (
@ -35,10 +38,13 @@ export function AccountSidebar() {
<SecondarySidebar>
{menu.map((item) => (
<AccountSidebarItem
<SecondarySidebarItem
key={item.label}
label={item.label}
route={item.route}
onClick={() => {
navigate(item.route);
}}
/>
))}
</SecondarySidebar>

View File

@ -20,15 +20,9 @@ import { useForm, joiResolver } from "@mantine/form";
import Joi from "joi";
import { Button } from "@atoms/Buttons";
import { Select } from "@atoms/Select";
import { TextInput } from "@atoms/TextInput";
import { SupportedCurrencies } from "@constants/currencies";
import { PaymentMethods as _PaymentMethods } from "@constants/payment-methods";
import { TextInput } from "@atoms/TextInput";
interface FormValues {
currency: string;
paymentMethod: string;
accountNumber: string;
}
export function AddPaymentMethod() {
const { getInputProps, onSubmit, setFieldValue, values } =
@ -41,7 +35,7 @@ export function AddPaymentMethod() {
},
});
const PaymentMethods = useMemo(() => {
const paymentMethods = useMemo(() => {
if (!values.currency) {
return [];
}
@ -61,6 +55,7 @@ export function AddPaymentMethod() {
.sort((a, b) => (a.label > b.label ? 1 : -1));
}, [values?.currency]);
// TODO @subir
const handleSubmit = (values: FormValues) => console.log(values);
useEffect(() => {
@ -86,7 +81,7 @@ export function AddPaymentMethod() {
<Collapse in={Boolean(values.currency)}>
<Select
creatable
data={PaymentMethods}
data={paymentMethods}
id="paymentMethod"
label="Select your preferred payment method"
placeholder="Pick one"
@ -114,6 +109,12 @@ export function AddPaymentMethod() {
);
}
interface FormValues {
currency: string;
paymentMethod: string;
accountNumber: string;
}
const schema = Joi.object<FormValues>({
currency: Joi.string().required(),
paymentMethod: Joi.string().required(),

View File

@ -28,8 +28,8 @@ exports[`organisms::AddPaymentMethod > renders without exploding 1`] = `
tabindex="-1"
>
<input
defaultvalue=""
type="hidden"
value=""
/>
<div
class="mantine-Select-wrapper mantine-12sbrde"
@ -40,11 +40,11 @@ exports[`organisms::AddPaymentMethod > renders without exploding 1`] = `
autocomplete="nope"
class="mantine-Select-defaultVariant mantine-Select-input mantine-93d3e4"
data-mantine-stop-propagation="false"
defaultvalue=""
id="currency"
placeholder="Pick one"
readonly=""
type="text"
value=""
/>
<div
class="mantine-Select-rightSection mantine-14dm59e"
@ -95,8 +95,8 @@ exports[`organisms::AddPaymentMethod > renders without exploding 1`] = `
tabindex="-1"
>
<input
defaultvalue=""
type="hidden"
value=""
/>
<div
class="mantine-Select-wrapper mantine-12sbrde"
@ -107,10 +107,10 @@ exports[`organisms::AddPaymentMethod > renders without exploding 1`] = `
autocomplete="nope"
class="mantine-Select-defaultVariant mantine-Select-input mantine-16ko5f2"
data-mantine-stop-propagation="false"
defaultvalue=""
id="paymentMethod"
placeholder="Pick one"
type="text"
value=""
/>
<div
class="mantine-Select-rightSection mantine-14dm59e"
@ -160,9 +160,9 @@ exports[`organisms::AddPaymentMethod > renders without exploding 1`] = `
<input
aria-invalid="false"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-dagq8e"
defaultvalue=""
id="accountNumber"
type="text"
value=""
/>
</div>
</div>

View File

@ -14,21 +14,16 @@
// limitations under the License.
// =============================================================================
import { Stack } from "@mantine/core";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Wallet } from ".";
import { ChangePassword } from ".";
export default {
title: "pages/Wallet",
component: Wallet,
} as ComponentMeta<typeof Wallet>;
title: "organisms/Change Password",
component: ChangePassword,
} as ComponentMeta<typeof ChangePassword>;
const Template: ComponentStory<typeof Wallet> = () => {
return (
<Stack>
<Wallet />
</Stack>
);
const Template: ComponentStory<typeof ChangePassword> = () => {
return <ChangePassword />;
};
export const Default = Template.bind({});

View File

@ -18,7 +18,7 @@ import { FormattedMessage } from "react-intl";
import { Stack, Box, Group } from "@mantine/core";
import { useForm, joiResolver } from "@mantine/form";
import { showNotification } from "@mantine/notifications";
import { TextInput } from "@atoms/TextInput";
import { PasswordInput } from "@atoms/PasswordInput";
import { LangKeys } from "@constants/lang";
import { Button } from "@atoms/Buttons";
import { useChangePassword } from "@hooks/storage/useChangePassword";
@ -68,9 +68,8 @@ export function ChangePassword() {
<Box>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack spacing="lg">
<TextInput
<PasswordInput
id="password"
type="password"
required
label={
<FormattedMessage
@ -80,10 +79,9 @@ export function ChangePassword() {
}
{...form.getInputProps("newPassword")}
/>
<TextInput
<PasswordInput
id="confirmPassword"
required
type="password"
label={
<FormattedMessage
id={LangKeys.AccountSecurityFieldRepeatPassword}
@ -92,9 +90,8 @@ export function ChangePassword() {
}
{...form.getInputProps("confirmPassword")}
/>
<TextInput
<PasswordInput
id="currentPassword"
type="password"
required
label={
<FormattedMessage

View File

@ -0,0 +1,33 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { SelectMoneroNode } from ".";
export default {
title: "organisms/Select Monero Node",
component: SelectMoneroNode,
} as ComponentMeta<typeof SelectMoneroNode>;
const Template: ComponentStory<typeof SelectMoneroNode> = (args) => {
return <SelectMoneroNode {...args} />;
};
export const Default = Template.bind({});
Default.args = {
onGoBack: () => console.log("go back"),
onNext: () => console.log("next"),
};

View File

@ -0,0 +1,33 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { SetPassword } from ".";
export default {
title: "organisms/Set Password",
component: SetPassword,
} as ComponentMeta<typeof SetPassword>;
const Template: ComponentStory<typeof SetPassword> = (args) => {
return <SetPassword {...args} />;
};
export const Default = Template.bind({});
Default.args = {
onGoBack: () => console.log("go back"),
onNext: (value: string) => console.log(value),
};

View File

@ -0,0 +1,86 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import { describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AppProviders } from "@atoms/AppProviders";
import { SetPassword } from ".";
describe("organisms::SetPassword", () => {
it("renders without exploding", () => {
const onBackSpy = vi.fn();
const onNextSpy = vi.fn();
const { asFragment, unmount } = render(
<AppProviders>
<SetPassword onGoBack={onBackSpy} onNext={onNextSpy} />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("calls onGoBack", async () => {
const onBackSpy = vi.fn();
const onNextSpy = vi.fn();
const { unmount } = render(
<AppProviders>
<SetPassword onGoBack={onBackSpy} onNext={onNextSpy} />
</AppProviders>
);
expect(onBackSpy).to.not.toHaveBeenCalled();
fireEvent.click(await screen.findByLabelText("Click to go back"));
expect(onBackSpy).to.toHaveBeenCalledTimes(1);
expect(onNextSpy).to.not.toHaveBeenCalled();
unmount();
});
it("blocks submit if validation fails", async () => {
const onBackSpy = vi.fn();
const onNextSpy = vi.fn();
const { unmount } = render(
<AppProviders>
<SetPassword onGoBack={onBackSpy} onNext={onNextSpy} />
</AppProviders>
);
expect(onNextSpy).to.not.toHaveBeenCalled();
fireEvent.click(await screen.findByLabelText("Click to submit"));
expect(onNextSpy).to.not.toHaveBeenCalled();
unmount();
});
it("calls onSubmit if validation succeeds", async () => {
const onBackSpy = vi.fn();
const onNextSpy = vi.fn();
const user = userEvent.setup();
const { unmount } = render(
<AppProviders>
<SetPassword onGoBack={onBackSpy} onNext={onNextSpy} />
</AppProviders>
);
await user.type(screen.getByLabelText("Enter password"), "Qwe$9999", {
skipAutoClose: true,
});
await user.type(screen.getByLabelText("Repeat password"), "Qwe$9999", {
skipAutoClose: true,
});
expect(onNextSpy).to.not.toHaveBeenCalled();
fireEvent.submit(screen.getByLabelText("Click to submit"));
expect(onNextSpy).to.toHaveBeenCalledTimes(1);
unmount();
});
});

View File

@ -23,9 +23,9 @@ import { Button, TextButton } from "@atoms/Buttons";
import { LangKeys } from "@constants/lang";
interface SetPasswordProps {
value: string;
onGoBack: () => void;
onNext: (password: string) => void;
value?: string;
}
export function SetPassword(props: SetPasswordProps) {
@ -43,7 +43,7 @@ export function SetPassword(props: SetPasswordProps) {
};
return (
<form onSubmit={onSubmit(handleSubmit)}>
<form data-testid="change-password-form" onSubmit={onSubmit(handleSubmit)}>
<Stack>
<Container>
<Heading order={1} stringId={LangKeys.CreatePassword}>
@ -55,21 +55,31 @@ export function SetPassword(props: SetPasswordProps) {
password.
</BodyText>
<TextInput
aria-label="Enter password"
autoFocus={false}
id="password"
label="Password"
tabIndex={1}
type="password"
{...getInputProps("password")}
/>
<TextInput
aria-label="Repeat password"
autoFocus={false}
id="repeatPassword"
label="Repeat password"
tabIndex={2}
type="password"
{...getInputProps("repeatPassword")}
/>
<Space h="lg" />
<Group position="apart">
<TextButton onClick={onGoBack}>Go Back</TextButton>
<Button type="submit">Next</Button>
<TextButton aria-label="Click to go back" onClick={onGoBack}>
Go Back
</TextButton>
<Button aria-label="Click to submit" type="submit">
Next
</Button>
</Group>
</Stack>
</form>

View File

@ -0,0 +1,109 @@
// Vitest Snapshot v1
exports[`organisms::SetPassword > renders without exploding 1`] = `
<DocumentFragment>
<form
data-testid="change-password-form"
>
<div
class="mantine-Stack-root mantine-lfk3cq"
>
<div
class="mantine-Container-root mantine-me1cdz"
>
<h1
class="mantine-Title-root mantine-1rawl7h"
>
Create password
</h1>
</div>
<div
class="mantine-Text-root mantine-1ourfup"
>
All your data is stored locally on your machine. Haveno uses solely a password.
</div>
<div
class="mantine-TextInput-root mantine-14qek68"
>
<label
class="mantine-TextInput-label mantine-1bjo575"
for="password"
id="password-label"
>
Password
</label>
<div
class="mantine-TextInput-wrapper mantine-12sbrde"
>
<input
aria-invalid="false"
aria-label="Enter password"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-nk8491"
id="password"
tabindex="1"
type="password"
value=""
/>
</div>
</div>
<div
class="mantine-TextInput-root mantine-14qek68"
>
<label
class="mantine-TextInput-label mantine-1bjo575"
for="repeatPassword"
id="repeatPassword-label"
>
Repeat password
</label>
<div
class="mantine-TextInput-wrapper mantine-12sbrde"
>
<input
aria-invalid="false"
aria-label="Repeat password"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-nk8491"
id="repeatPassword"
tabindex="2"
type="password"
value=""
/>
</div>
</div>
<div
class="mantine-63n06h"
/>
<div
class="mantine-Group-root mantine-19jxmdp"
>
<button
aria-label="Click to go back"
class="mantine-UnstyledButton-root mantine-Group-child mantine-1smyfmn"
type="button"
>
<span
class="mantine-Text-root mantine-1mwiqnz"
>
Go Back
</span>
</button>
<button
aria-label="Click to submit"
class="mantine-Button-filled mantine-Button-root mantine-Group-child mantine-s2bdxi"
type="submit"
>
<div
class="mantine-3xbgk5 mantine-Button-inner"
>
<span
class="mantine-qo1k2 mantine-Button-label"
>
Next
</span>
</div>
</button>
</div>
</div>
</form>
</DocumentFragment>
`;

View File

@ -0,0 +1,33 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { SetPrimaryFiat } from ".";
export default {
title: "organisms/Set Primary Fiat",
component: SetPrimaryFiat,
} as ComponentMeta<typeof SetPrimaryFiat>;
const Template: ComponentStory<typeof SetPrimaryFiat> = (args) => {
return <SetPrimaryFiat {...args} />;
};
export const Default = Template.bind({});
Default.args = {
onGoBack: () => console.log("go back"),
onNext: (value: string) => console.log(value),
};

View File

@ -25,7 +25,7 @@ import { SupportedCurrencies } from "@constants/currencies";
interface SetSetPrimaryFiatProps {
onGoBack: () => void;
onNext: (fiat: string) => void;
value: string;
value?: string;
}
export function SetPrimaryFiat(props: SetSetPrimaryFiatProps) {

View File

@ -14,12 +14,23 @@
// limitations under the License.
// =============================================================================
import { describe, expect, it } from "vitest";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { render } from "@testing-library/react";
import { AppProviders } from "@atoms/AppProviders";
import { SyncStatus } from "@constants/sync-status";
import { Sidebar } from ".";
describe("molecules::Sidebar", () => {
beforeAll(() => {
vi.mock("@hooks/haveno/useSyncStatus", () => ({
useSyncStatus: () => ({
isLoading: false,
isSuccess: true,
data: SyncStatus.NotSynced,
}),
}));
});
it("renders without exploding", () => {
const { asFragment } = render(
<AppProviders>

View File

@ -16,7 +16,7 @@
import { Group, createStyles, Box } from "@mantine/core";
import { NavbarLayout } from "@templates/NavbarLayout";
import { AccountSidebar } from "@molecules/AccountSidebar";
import { AccountSidebar } from "@organisms/AccountSidebar";
interface AccountContentProps {
children: JSX.Element | JSX.Element[];

View File

@ -19,7 +19,9 @@ import { ReactComponent as EthLogo } from "@assets/eth.svg";
import { ReactComponent as EurLogo } from "@assets/eur.svg";
import { PaymentMethodIds } from "./payment-methods";
export type SupportedFiat = "USD" | "ETH" | "GBP";
export type SupportedFiat = "USD" | "EUR" | "GBP";
export type SupportedCrypto = "XMR" | "BTC" | "ETH";
export const SupportedCurrencies = [
{

View File

@ -21,7 +21,12 @@ import { useHavenoClient } from "./useHavenoClient";
export function useIsMoneroNodeRunning() {
const client = useHavenoClient();
return useQuery<boolean, Error>(QueryKeys.MoneroNodeIsRunning, () =>
client.isMoneroNodeRunning()
);
return useQuery<boolean, Error>(QueryKeys.MoneroNodeIsRunning, async () => {
try {
const value = await client.isMoneroNodeRunning();
return value;
} catch {
return false;
}
});
}

View File

@ -14,12 +14,13 @@
// limitations under the License.
// =============================================================================
import { useMemo } from "react";
import { useResolvedPath, useLocation } from "react-router-dom";
interface LinkItemActiveProps {
to: string;
caseSensitive?: boolean;
end?: boolean;
to?: string;
}
/**
@ -31,22 +32,29 @@ export const useNavLinkActive = ({
caseSensitive = false,
end = false,
to,
}: LinkItemActiveProps) => {
}: LinkItemActiveProps): boolean => {
const location = useLocation();
const path = useResolvedPath(to);
const path = useResolvedPath(to ?? "");
let locationPathname = location.pathname;
let toPathname = path.pathname;
const isActive = useMemo(() => {
if (!to) {
return false;
}
let locationPathName = location.pathname;
let toPathName = path.pathname;
if (!caseSensitive) {
locationPathname = locationPathname.toLowerCase();
toPathname = toPathname.toLowerCase();
locationPathName = locationPathName.toLowerCase();
toPathName = toPathName.toLowerCase();
}
return (
locationPathname === toPathname ||
locationPathName === toPathName ||
(!end &&
locationPathname.startsWith(toPathname) &&
locationPathname.charAt(toPathname.length) === "/")
locationPathName.startsWith(toPathName) &&
locationPathName.charAt(toPathName.length) === "/")
);
}, [location, path, caseSensitive, end, to]);
return isActive;
};

View File

@ -15,11 +15,11 @@
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { validateSession } from "@src/utils/session";
import { validateSession } from "@utils/session";
import { useQuery } from "react-query";
export function useAuth() {
return useQuery(
return useQuery<boolean, Error>(
QueryKeys.AuthSession,
async () => {
if (await validateSession()) {

View File

@ -14,10 +14,10 @@
// limitations under the License.
// =============================================================================
import { QueryKeys } from "@constants/query-keys";
import { getIpcError } from "@src/utils/get-ipc-error";
import { createSession } from "@src/utils/session";
import { useMutation, useQueryClient } from "react-query";
import { QueryKeys } from "@constants/query-keys";
import { getIpcError } from "@utils/get-ipc-error";
import { createSession } from "@utils/session";
interface Variables {
currentPassword: string;

View File

@ -17,7 +17,7 @@
import { createStyles } from "@mantine/core";
import { FormattedMessage } from "react-intl";
import { LangKeys } from "@constants/lang";
import { NodeConnectSwitch } from "@atoms/NodeConnectSwitch";
import { NodeConnectSwitch } from "@molecules/NodeConnectSwitch";
import { ReactComponent as CloudIcon } from "@assets/setting-cloud.svg";
import { ReactComponent as ServerIcon } from "@assets/setting-server.svg";
import { NodeLocalForm } from "./NodeLocalForm";

View File

@ -15,20 +15,15 @@
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { AppProviders } from "@atoms/AppProviders";
import { Home } from ".";
export default {
title: "pages/Onboarding/Home",
title: "pages/Home",
component: Home,
} as ComponentMeta<typeof Home>;
const Template: ComponentStory<typeof Home> = () => {
return (
<AppProviders>
<Home />
</AppProviders>
);
return <Home />;
};
export const Default = Template.bind({});

View File

@ -14,8 +14,17 @@
// limitations under the License.
// =============================================================================
import { NavbarLayout } from "@templates/NavbarLayout";
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Login } from ".";
export function Wallet() {
return <NavbarLayout></NavbarLayout>;
}
export default {
title: "pages/Login",
component: Login,
} as ComponentMeta<typeof Login>;
const Template: ComponentStory<typeof Login> = () => {
return <Login />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,113 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
const navSpy = vi.fn();
const loginSpy = vi.fn();
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AppProviders } from "@atoms/AppProviders";
import { Login } from ".";
import { ROUTES } from "@constants/routes";
describe("pages::Login", () => {
beforeEach(() => {
vi.mock("react-router-dom", async () => ({
...(await vi.importActual<any>("react-router-dom")), // eslint-disable-line @typescript-eslint/no-explicit-any
useNavigate: () => navSpy,
}));
vi.mock("@hooks/session/useLogin", () => ({
useLogin: () => ({
mutate: loginSpy,
isLoading: false,
}),
}));
});
afterEach(() => {
vi.clearAllMocks();
});
it("renders without exploding", () => {
const { asFragment, unmount } = render(
<AppProviders>
<Login />
</AppProviders>
);
expect(asFragment()).toMatchSnapshot();
unmount();
});
it("blocks login if validation fails", async () => {
const user = userEvent.setup();
const { unmount } = render(
<AppProviders>
<Login />
</AppProviders>
);
const btnSubmit = screen.getByRole("button", { name: "Login" });
fireEvent.submit(btnSubmit);
expect(loginSpy).to.not.toHaveBeenCalled();
// try a short password
await user.type(screen.getByLabelText("Password"), "foo");
fireEvent.submit(btnSubmit);
expect(loginSpy).to.not.toHaveBeenCalled();
unmount();
});
it("calls login", async () => {
const PASSWORD = "Haveno!2022";
const user = userEvent.setup();
const { unmount } = render(
<AppProviders>
<Login />
</AppProviders>
);
expect(loginSpy).to.not.toHaveBeenCalled();
await user.type(screen.getByLabelText("Password"), PASSWORD);
fireEvent.submit(screen.getByRole("button", { name: "Login" }));
expect(loginSpy).to.toHaveBeenCalledTimes(1);
unmount();
});
// TODO: update behavior to redirect to the Market page
it("navigates to Payment Accounts page after successful login", async () => {
const PASSWORD = "Haveno!2022";
loginSpy.mockImplementation(({ password }, { onSuccess, onError }) => {
if (password === PASSWORD) {
onSuccess();
} else {
onError({ message: "Invalid password" });
}
});
const user = userEvent.setup();
const { unmount } = render(
<AppProviders>
<Login />
</AppProviders>
);
expect(navSpy).to.toHaveBeenCalledTimes(0);
await user.type(screen.getByLabelText("Password"), PASSWORD);
fireEvent.submit(screen.getByRole("button", { name: "Login" }));
expect(navSpy).to.toHaveBeenCalledTimes(1);
expect(navSpy).toHaveBeenCalledWith(ROUTES.AccountPaymentAccounts, {
replace: true,
});
unmount();
});
});

View File

@ -19,12 +19,12 @@ import { joiResolver, useForm } from "@mantine/form";
import { useNavigate } from "react-router-dom";
import { Container, Group, Space, Stack } from "@mantine/core";
import { showNotification } from "@mantine/notifications";
import { useLogin } from "@hooks/session/useLogin";
import { CenteredLayout } from "@templates/CenteredLayout";
import { BodyText, Heading } from "@atoms/Typography";
import { ROUTES } from "@constants/routes";
import { useLogin } from "@hooks/session/useLogin";
import { Button } from "@atoms/Buttons";
import { TextInput } from "@atoms/TextInput";
import { ROUTES } from "@constants/routes";
import { CONTENT_MAX_WIDTH } from "./_constants";
export function Login() {
@ -67,6 +67,7 @@ export function Login() {
</BodyText>
<Space h="lg" />
<TextInput
aria-label="Password"
id="password"
label="Password"
type="password"

View File

@ -0,0 +1,94 @@
// Vitest Snapshot v1
exports[`pages::Login > renders without exploding 1`] = `
<DocumentFragment>
<div
class="mantine-Stack-root mantine-14yk9bp"
>
<header
class="mantine-caub1y"
>
<img
alt="Haveno"
height="24"
src="/assets/logo.svg"
/>
</header>
<div
class="mantine-Container-root mantine-1hpmr7z"
>
<div
class="mantine-Stack-root mantine-12lpdrc"
>
<form>
<div
class="mantine-Stack-root mantine-lfk3cq"
>
<div
class="mantine-Container-root mantine-me1cdz"
>
<h1
class="mantine-Title-root mantine-1rawl7h"
>
Login to Haveno
</h1>
</div>
<div
class="mantine-Text-root mantine-1ourfup"
>
All your data is stored locally on your machine. Haveno uses solely a password.
</div>
<div
class="mantine-63n06h"
/>
<div
class="mantine-TextInput-root mantine-14qek68"
>
<label
class="mantine-TextInput-label mantine-1bjo575"
for="password"
id="password-label"
>
Password
</label>
<div
class="mantine-TextInput-wrapper mantine-12sbrde"
>
<input
aria-invalid="false"
aria-label="Password"
class="mantine-TextInput-defaultVariant mantine-TextInput-input mantine-nk8491"
id="password"
type="password"
value=""
/>
</div>
</div>
<div
class="mantine-63n06h"
/>
<div
class="mantine-Group-root mantine-19jxmdp"
>
<button
class="mantine-Button-filled mantine-Button-root mantine-Group-child mantine-s2bdxi"
type="submit"
>
<div
class="mantine-3xbgk5 mantine-Button-inner"
>
<span
class="mantine-qo1k2 mantine-Button-label"
>
Login
</span>
</div>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,30 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { ConnectingMonero } from ".";
export default {
title: "pages/Onboarding/Connecting Monero",
component: ConnectingMonero,
} as ComponentMeta<typeof ConnectingMonero>;
const Template: ComponentStory<typeof ConnectingMonero> = () => {
return <ConnectingMonero />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -0,0 +1,30 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { CreateAccount } from ".";
export default {
title: "pages/Onboarding/Create Account",
component: CreateAccount,
} as ComponentMeta<typeof CreateAccount>;
const Template: ComponentStory<typeof CreateAccount> = () => {
return <CreateAccount />;
};
export const Default = Template.bind({});
Default.args = {};

View File

@ -15,7 +15,6 @@
// =============================================================================
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { AppProviders } from "@atoms/AppProviders";
import { Welcome } from ".";
export default {
@ -24,11 +23,7 @@ export default {
} as ComponentMeta<typeof Welcome>;
const Template: ComponentStory<typeof Welcome> = () => {
return (
<AppProviders>
<Welcome />
</AppProviders>
);
return <Welcome />;
};
export const Default = Template.bind({});

View File

@ -14,5 +14,6 @@
// limitations under the License.
// =============================================================================
export * from "./Welcome";
export * from "./ConnectingMonero";
export * from "./CreateAccount";
export * from "./Welcome";

View File

@ -32,7 +32,8 @@
"src/**/*.tsx",
"types/**/*.d.ts",
"../../types/**/*.d.ts",
"../preload/contracts.d.ts"
"../preload/contracts.d.ts",
"../../tests/setup-tests.ts"
],
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
}

View File

@ -14,19 +14,24 @@
// limitations under the License.
// =============================================================================
import { mergeConfig } from "vite";
import viteConfig from "./vite.config";
/**
* Config for global end-to-end tests
* placed in project root tests folder
* @type {import('vite').UserConfig}
* @see https://vitest.dev/config/
*/
const config = {
const config = mergeConfig(viteConfig, {
test: {
setupFiles: ["../../tests/setup-tests.ts"],
environment: "jsdom",
include: ["./src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
reporter: ["html"],
},
},
};
});
export default config;

30
tests/setup-tests.ts Normal file
View File

@ -0,0 +1,30 @@
// =============================================================================
// Copyright 2022 Haveno
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =============================================================================
/* eslint-disable @typescript-eslint/no-namespace,@typescript-eslint/no-explicit-any */
import { expect } from "vitest";
import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
import matchers from "@testing-library/jest-dom/matchers";
declare global {
namespace Vi {
interface JestAssertion<T = any>
extends jest.Matchers<void, T>,
TestingLibraryMatchers<T, void> {}
}
}
expect.extend(matchers);

315
yarn.lock
View File

@ -1074,7 +1074,7 @@
pirates "^4.0.5"
source-map-support "^0.5.16"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.8", "@babel/runtime@^7.17.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.8", "@babel/runtime@^7.17.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
@ -2291,10 +2291,10 @@
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
"@storybook/builder-vite@^0.1.29":
version "0.1.29"
resolved "https://registry.yarnpkg.com/@storybook/builder-vite/-/builder-vite-0.1.29.tgz#ca7dff079968605e323b365d72c28903e4a072eb"
integrity sha512-WMPY1Pd5Da3BdXDfgFNhIWq09i7oxMT06nxS909VKOOKHZvckmfQpS8iKJLYp730t4t7S3+MtHf/t2+Kr7Cxew==
"@storybook/builder-vite@^0.1.34":
version "0.1.34"
resolved "https://registry.yarnpkg.com/@storybook/builder-vite/-/builder-vite-0.1.34.tgz#9d65a1b430967cae688ec7cb586056e53f1afef2"
integrity sha512-a40uMOAu66W4zX9+k+wGhhfsyJWSXb7i4Ve5rS9I1fZ/d7xvsjhgQzhHZtLqESU1A/CO1gyvexbcw44efMPuGQ==
dependencies:
"@joshwooding/vite-plugin-react-docgen-typescript" "0.0.4"
"@mdx-js/mdx" "^1.6.22"
@ -3018,6 +3018,21 @@
lz-string "^1.4.4"
pretty-format "^27.0.2"
"@testing-library/jest-dom@^5.16.4":
version "5.16.4"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz#938302d7b8b483963a3ae821f1c0808f872245cd"
integrity sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==
dependencies:
"@babel/runtime" "^7.9.2"
"@types/testing-library__jest-dom" "^5.9.1"
aria-query "^5.0.0"
chalk "^3.0.0"
css "^3.0.0"
css.escape "^1.5.1"
dom-accessibility-api "^0.5.6"
lodash "^4.17.15"
redent "^3.0.0"
"@testing-library/react@^12":
version "12.1.5"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b"
@ -3032,6 +3047,11 @@
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.1.1.tgz#e1ff6118896e4b22af31e5ea2f9da956adde23d8"
integrity sha512-XrjH/iEUqNl9lF2HX9YhPNV7Amntkcnpw0Bo1KkRzowNDcgSN9i0nm4Q8Oi5wupgdfPaJNMAWa61A+voD6Kmwg==
"@testing-library/user-event@^14.2.0":
version "14.2.0"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.2.0.tgz#8293560f8f80a00383d6c755ec3e0b918acb1683"
integrity sha512-+hIlG4nJS6ivZrKnOP7OGsDu9Fxmryj9vCl8x0ZINtTJcCHs2zLsYif5GzuRiBF2ck5GZG2aQr7Msg+EHlnYVQ==
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@ -3173,6 +3193,14 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@*":
version "27.5.1"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.1.tgz#2c8b6dc6ff85c33bcd07d0b62cb3d19ddfdb3ab9"
integrity sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ==
dependencies:
jest-matcher-utils "^27.0.0"
pretty-format "^27.0.0"
"@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
@ -3360,6 +3388,13 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310"
integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==
"@types/testing-library__jest-dom@^5.9.1":
version "5.14.3"
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz#ee6c7ffe9f8595882ee7bda8af33ae7b8789ef17"
integrity sha512-oKZe+Mf4ioWlMuzVBaXQ9WDnEm1+umLx0InILg+yvZVBBDmzV5KfZyLrCvadtWcx8+916jLmHafcmqqffl+iIw==
dependencies:
"@types/jest" "*"
"@types/uglify-js@*":
version "3.13.2"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.2.tgz#1044c1713fb81cb1ceef29ad8a9ee1ce08d690ef"
@ -3741,6 +3776,11 @@ JSONStream@^1.0.4:
jsonparse "^1.2.0"
through ">=2.2.7 <3"
abab@^2.0.5, abab@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
accepts@~1.3.5, accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
@ -3749,12 +3789,20 @@ accepts@~1.3.5, accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
acorn-globals@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==
dependencies:
acorn "^7.1.1"
acorn-walk "^7.1.1"
acorn-jsx@^5.3.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn-walk@^7.2.0:
acorn-walk@^7.1.1, acorn-walk@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
@ -3769,7 +3817,7 @@ acorn@^6.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
acorn@^7.4.1:
acorn@^7.1.1, acorn@^7.4.1:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
@ -4562,6 +4610,11 @@ brorand@^1.0.1, brorand@^1.1.0:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
browser-process-hrtime@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
version "1.2.0"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
@ -4952,6 +5005,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@ -5686,11 +5747,37 @@ css.escape@^1.5.1:
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=
css@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d"
integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==
dependencies:
inherits "^2.0.4"
source-map "^0.6.1"
source-map-resolve "^0.6.0"
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
cssom@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36"
integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==
cssom@~0.3.6:
version "0.3.8"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
cssstyle@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
dependencies:
cssom "~0.3.6"
csstype@^2.5.7:
version "2.6.20"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
@ -5711,6 +5798,15 @@ dargs@^7.0.0:
resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc"
integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==
data-urls@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==
dependencies:
abab "^2.0.6"
whatwg-mimetype "^3.0.0"
whatwg-url "^11.0.0"
dayjs@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.0.tgz#009bf7ef2e2ea2d5db2e6583d2d39a4b5061e805"
@ -5764,6 +5860,11 @@ decamelize@^1.1.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
decimal.js@^10.3.1:
version "10.3.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@ -5898,6 +5999,11 @@ detect-port@^1.3.0:
address "^1.0.1"
debug "^2.6.0"
diff-sequences@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@ -5978,6 +6084,11 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-accessibility-api@^0.5.6:
version "0.5.14"
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56"
integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==
dom-accessibility-api@^0.5.9:
version "0.5.13"
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz#102ee5f25eacce09bdf1cfa5a298f86da473be4b"
@ -6022,6 +6133,13 @@ domelementtype@^2.0.1, domelementtype@^2.2.0:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
domexception@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673"
integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==
dependencies:
webidl-conversions "^7.0.0"
domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
@ -8035,6 +8153,13 @@ hosted-git-info@^4.0.1, hosted-git-info@^4.0.2:
dependencies:
lru-cache "^6.0.0"
html-encoding-sniffer@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==
dependencies:
whatwg-encoding "^2.0.0"
html-entities@^2.1.0:
version "2.3.3"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
@ -8173,7 +8298,7 @@ iconv-lite@0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
iconv-lite@^0.6.2:
iconv-lite@0.6.3, iconv-lite@^0.6.2:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@ -8655,6 +8780,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
is-redirect@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
@ -8865,6 +8995,21 @@ jake@^10.8.5:
filelist "^1.0.1"
minimatch "^3.0.4"
jest-diff@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==
dependencies:
chalk "^4.0.0"
diff-sequences "^27.5.1"
jest-get-type "^27.5.1"
pretty-format "^27.5.1"
jest-get-type@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
jest-haste-map@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa"
@ -8886,6 +9031,16 @@ jest-haste-map@^26.6.2:
optionalDependencies:
fsevents "^2.1.2"
jest-matcher-utils@^27.0.0:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab"
integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==
dependencies:
chalk "^4.0.0"
jest-diff "^27.5.1"
jest-get-type "^27.5.1"
pretty-format "^27.5.1"
jest-mock@^27.0.6:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6"
@ -8974,6 +9129,39 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
jsdom@^19.0.0:
version "19.0.0"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-19.0.0.tgz#93e67c149fe26816d38a849ea30ac93677e16b6a"
integrity sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==
dependencies:
abab "^2.0.5"
acorn "^8.5.0"
acorn-globals "^6.0.0"
cssom "^0.5.0"
cssstyle "^2.3.0"
data-urls "^3.0.1"
decimal.js "^10.3.1"
domexception "^4.0.0"
escodegen "^2.0.0"
form-data "^4.0.0"
html-encoding-sniffer "^3.0.0"
http-proxy-agent "^5.0.0"
https-proxy-agent "^5.0.0"
is-potential-custom-element-name "^1.0.1"
nwsapi "^2.2.0"
parse5 "6.0.1"
saxes "^5.0.1"
symbol-tree "^3.2.4"
tough-cookie "^4.0.0"
w3c-hr-time "^1.0.2"
w3c-xmlserializer "^3.0.0"
webidl-conversions "^7.0.0"
whatwg-encoding "^2.0.0"
whatwg-mimetype "^3.0.0"
whatwg-url "^10.0.0"
ws "^8.2.3"
xml-name-validator "^4.0.0"
jsesc@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@ -10104,6 +10292,11 @@ num2fraction@^1.2.2:
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
nwsapi@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -10458,7 +10651,7 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
parse5@^6.0.0:
parse5@6.0.1, parse5@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
@ -10809,7 +11002,7 @@ pretty-error@^2.1.1:
lodash "^4.17.20"
renderkid "^2.0.4"
pretty-format@^27.0.2:
pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
@ -10942,6 +11135,11 @@ pseudomap@^1.0.2:
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
psl@^1.1.33:
version "1.8.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
public-encrypt@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@ -10989,7 +11187,7 @@ punycode@^1.2.4:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
punycode@^2.1.0:
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@ -11814,6 +12012,13 @@ sax@^1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
saxes@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
dependencies:
xmlchars "^2.2.0"
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
@ -12157,6 +12362,14 @@ source-map-resolve@^0.5.0:
source-map-url "^0.4.0"
urix "^0.1.0"
source-map-resolve@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2"
integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==
dependencies:
atob "^2.1.2"
decode-uri-component "^0.2.0"
source-map-support@^0.5.16, source-map-support@^0.5.19, source-map-support@~0.5.12, source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
@ -12514,6 +12727,11 @@ svg-parser@^2.0.2:
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
symbol.prototype.description@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/symbol.prototype.description/-/symbol.prototype.description-1.0.5.tgz#d30e01263b6020fbbd2d2884a6276ce4d49ab568"
@ -12545,6 +12763,11 @@ synchronous-promise@^2.0.15:
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.15.tgz#07ca1822b9de0001f5ff73595f3d08c4f720eb8e"
integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg==
tabler-icons-react@^1.48.0:
version "1.48.0"
resolved "https://registry.yarnpkg.com/tabler-icons-react/-/tabler-icons-react-1.48.0.tgz#2b3251d4b9effa1e78baf4cb05fe7cf79449f116"
integrity sha512-dlcAIGYIB7+fsU1tj8HuK5aN57g3Q5KD8GMAxpBR9E62yFhjn8fbwD2M2X18E66v5TYakUhu6tfUxM+/jvI6Kg==
tapable@^1.0.0, tapable@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
@ -12799,6 +13022,22 @@ toidentifier@1.0.1:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
tough-cookie@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==
dependencies:
psl "^1.1.33"
punycode "^2.1.1"
universalify "^0.1.2"
tr46@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9"
integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
dependencies:
punycode "^2.1.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
@ -13137,7 +13376,7 @@ unist-util-visit@2.0.3, unist-util-visit@^2.0.0:
unist-util-is "^4.0.0"
unist-util-visit-parents "^3.0.0"
universalify@^0.1.0:
universalify@^0.1.0, universalify@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
@ -13463,6 +13702,20 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
w3c-hr-time@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
dependencies:
browser-process-hrtime "^1.0.0"
w3c-xmlserializer@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923"
integrity sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==
dependencies:
xml-name-validator "^4.0.0"
walker@^1.0.7, walker@~1.0.5:
version "1.0.8"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"
@ -13603,11 +13856,39 @@ whatwg-encoding@^1.0.5:
dependencies:
iconv-lite "0.4.24"
whatwg-encoding@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==
dependencies:
iconv-lite "0.6.3"
whatwg-mimetype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-mimetype@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
whatwg-url@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-10.0.0.tgz#37264f720b575b4a311bd4094ed8c760caaa05da"
integrity sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==
dependencies:
tr46 "^3.0.0"
webidl-conversions "^7.0.0"
whatwg-url@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
dependencies:
tr46 "^3.0.0"
webidl-conversions "^7.0.0"
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
@ -13744,6 +14025,11 @@ xdg-basedir@^4.0.0:
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
xml-name-validator@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
xmlbuilder@>=11.0.1:
version "15.1.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"
@ -13754,6 +14040,11 @@ xmlbuilder@^9.0.7:
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
xmlchars@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"