Skip to content

Commit

Permalink
coral: implement form component [#141] (#159)
Browse files Browse the repository at this point in the history
* feat: Add app directory and restructure related files.

* feat: Change unused vars to error for linting.

* feat: Add Form component based on aiven-core implementation.

* feat: Add userEvent package.

@testing-library/user-event has a peerDep for @testing-library/dom
but it's a different version than the one that @testing-library/react
uses and brings in as a devDep. Solution based on research on GitHub
shows we can either ignore the warning in pnpm or install the
@testing-library/dom as devDep in the version that @testing-library/react
uses it. I did the latter hoping that in one of the next versions of
@testing-library/user-event this will be resolved.

see
testing-library/user-event#438 (comment)
testing-library/user-event#551 (comment)

* feat: Add tests for Form based on aiven-core.

* feat: Introduce explicit typing for Form.

- remove use of 'any' type
- add stricter rule for use of any for linting

* feat: Update lodash usage to match dependencies.

* Remove comment about aiven-core mirroring

* Remove unused library.

* Rename and restructure pages files.

- `index.tsx` should be reserved for resource root
- files for pages can live on first level in `pages`
- rename files to match  naming conventions for react
- update documentation to be more explicit and correct

* Add login page to routing.

* Reorder lint rules after rebase.
  • Loading branch information
programmiri authored Nov 1, 2022
1 parent bf9bb65 commit 57eb19c
Show file tree
Hide file tree
Showing 17 changed files with 460 additions and 28 deletions.
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
│ │ ├── 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

0 comments on commit 57eb19c

Please sign in to comment.