Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

coral: implement form component [#141] #159

Merged
merged 12 commits into from
Nov 1, 2022
Merged
7 changes: 5 additions & 2 deletions coral/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ module.exports = {
"plugins": [
"react",
"@typescript-eslint",
"no-relative-import-paths"
"no-relative-import-paths",
],
"settings": {
"react": {
Expand All @@ -81,6 +81,9 @@ module.exports = {
"no-relative-import-paths/no-relative-import-paths": [
"error"
],
"no-restricted-imports": strip_ids_from_no_restricted_imports(NO_RESTRICTED_IMPORTS_RULES)
"no-unused-vars": "off",
"no-restricted-imports": strip_ids_from_no_restricted_imports(NO_RESTRICTED_IMPORTS_RULES),
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "error"
}
}
9 changes: 5 additions & 4 deletions coral/docs/directory-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ The structure is inspired in big parts by:
│ │ ├── ...
│ │ ├── index.ts
│ │── pages/
│ │ ├── page-one
│ │ ├── ...
│ │ └── index.ts
│ │ ├── index.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think want to even try to keep this up to date with current implementation so lets keep the examples abstract.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we can do this later 👍

│ │ ├── Login.tsx
│ │ ├── Users.tsx
│ │ └── ...
│ └── router.tsx
├── domain/
│ ├── name-of-domain-one/
Expand Down Expand Up @@ -127,7 +128,7 @@ In this directory, we group similar code together based on one feature. The stru

#### `pages`

Contains every page of the application, one file per page. The structure in this folder should mirror the structure of the web apps views and routing. If there is a link to a "dashboard" page in the web app, there should be a `Dashboard` page inside `pages`.
Contains every page of the application, one file per page. The structure in this folder should mirror the structure of the web apps views and routing. If there is a link to a "dashboard" page in the web app, there should be a `Dashboard` page inside `pages`. The files don't need to have a `Page` pre- or postfix since the directory already gives that information.

#### `services`

Expand Down
2 changes: 1 addition & 1 deletion coral/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="module" src="/src/app/main.tsx"></script>
</body>
</html>
12 changes: 9 additions & 3 deletions coral/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,21 @@
]
},
"dependencies": {
"@aivenio/design-system": "^18.4.3",
"@aivenio/design-system": "^19.0.1",
"@hookform/resolvers": "^2.9.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.2"
"react-hook-form": "^7.38.0",
"react-router-dom": "^6.4.2",
"zod": "^3.19.1"
},
"devDependencies": {
"@testing-library/dom": "^8.19.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.2.0",
"@types/lodash": "^4.14.182",
"@types/node": "*",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
Expand All @@ -53,7 +59,7 @@
"jest": "^29.2.1",
"jest-environment-jsdom": "^29.2.1",
"lint-staged": "^13.0.3",
"lodash": "4.x",
"lodash": "^4.17.21",
"prettier": "^2.7.1",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
Expand Down
56 changes: 51 additions & 5 deletions coral/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

153 changes: 153 additions & 0 deletions coral/src/app/components/Form.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Button } from "@aivenio/design-system";
import type { RenderResult } from "@testing-library/react";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import type { DeepPartial, FieldValues } from "react-hook-form";
import {
Form,
SubmitErrorHandler,
SubmitHandler,
TextInput,
useForm,
} from "src/app/components/Form";
import { z, ZodSchema } from "zod";

type WrapperProps<T extends FieldValues> = {
schema: ZodSchema;
defaultValues?: DeepPartial<T>;
onSubmit: SubmitHandler<T>;
onError: SubmitErrorHandler<T>;
};

const Wrapper = <T extends FieldValues>({
schema,
defaultValues,
onSubmit,
onError,
children,
}: React.PropsWithChildren<WrapperProps<T>>): React.ReactElement => {
const form = useForm<T>({ schema, defaultValues });
return (
<Form onSubmit={onSubmit} onError={onError} {...form}>
{children}
</Form>
);
};

describe("Form", () => {
const onSubmit = jest.fn();
const onError = jest.fn();
let results: RenderResult;
let user: ReturnType<typeof userEvent.setup>;

beforeEach(() => {
user = userEvent.setup();
});

afterEach(() => {
cleanup();
onSubmit.mockClear();
onError.mockClear();
});

const renderForm = <T extends FieldValues>(
children: React.ReactNode,
{
schema,
defaultValues,
}: { schema: ZodSchema; defaultValues?: DeepPartial<T> }
) => {
return render(
<Wrapper<T>
schema={schema}
defaultValues={defaultValues}
onSubmit={onSubmit}
onError={onError}
>
{children}
<Button type="submit" title="Submit" />
</Wrapper>
);
};

const typeText = async (value: string) => {
const el = screen.getByRole("textbox");
await user.clear(el);
await user.type(el, value);
};

const submit = async () => {
await user.click(screen.getByRole("button", { name: "Submit" }));
};

const assertSubmitted = (data: Record<string, string>) => {
expect(onSubmit).toHaveBeenCalledWith(data, expect.anything());
};

describe("<Form>", () => {
const schema = z.object({ name: z.string().min(3, "error") });
type Schema = z.infer<typeof schema>;

beforeEach(() => {
results = renderForm(
<TextInput<Schema> name="name" labelText="TextInput" />,
{ schema }
);
});

it("should call onSubmit() after submit if there are no validation errors", async () => {
await user.type(screen.getByLabelText("TextInput"), "abc");
await submit();
await waitFor(() =>
expect(onSubmit).toHaveBeenCalledWith(
{ name: "abc" },
expect.anything()
)
);
});

it("should call onError() after submit if there are validation errors", async () => {
await user.type(screen.getByLabelText("TextInput"), "a");
await submit();
await waitFor(() => expect(onError).toHaveBeenCalled());
expect(onError.mock.calls[0][0]).toMatchObject({
name: { message: "error" },
});
});
});

describe("<TextInput>", () => {
const schema = z.object({ name: z.string().min(3, "error") });
type Schema = z.infer<typeof schema>;

beforeEach(() => {
results = renderForm(
<TextInput<Schema> name="name" labelText="TextInput" />,
{ schema }
);
});

it("should render <TextInput>", () => {
expect(results.container).toMatchSnapshot();
});

it("should render label", () => {
expect(screen.queryByLabelText("TextInput")).toBeVisible();
});

it("should sync value to form state", async () => {
await typeText("value{tab}");
await submit();
assertSubmitted({ name: "value" });
});

it("should render errors after blur event and hide them after valid input", async () => {
await typeText("a{tab}");
await waitFor(() => expect(screen.queryByText("error")).toBeVisible());

await typeText("abc{tab}");
await waitFor(() => expect(screen.queryByText("error")).toBeNull());
});
});
});
Loading