diff --git a/.storybook/main.ts b/.storybook/main.ts index f9cb98b..81dfbbd 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,4 +1,5 @@ import type { StorybookConfig } from "@storybook/react-vite"; +import { mergeConfig, mergeAlias } from "vite"; const config: StorybookConfig = { stories: [ @@ -11,13 +12,29 @@ const config: StorybookConfig = { "@storybook/addon-essentials", "@storybook/addon-onboarding", "@storybook/addon-interactions", + "@storybook/addon-a11y", + "@storybook/addon-actions", ], + core: {}, framework: { name: "@storybook/react-vite", - options: {}, + options: { + builder: { + viteConfigPath: "./.storybook/sbvite.config.ts", + }, + }, }, docs: { autodocs: "tag", }, + async viteFinal(config) { + return mergeConfig(config, { + ...config, + resolve: { + ...config.resolve, + alias: [{ find: "~", replacement: "/app" }], + }, + }); + }, }; export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts index a72df87..543dde1 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -3,7 +3,7 @@ import "../app/tailwind.css"; const preview: Preview = { parameters: { - actions: { argTypesRegex: "^on[A-Z].*" }, + actions: {}, controls: { matchers: { color: /(background|color)$/i, diff --git a/.storybook/sbvite.config.ts b/.storybook/sbvite.config.ts new file mode 100644 index 0000000..e96c8e6 --- /dev/null +++ b/.storybook/sbvite.config.ts @@ -0,0 +1,12 @@ +import tsConfigPaths from "vite-tsconfig-paths"; +import { defineConfig, loadEnv } from "vite"; +import path from "path"; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); + process.env = { ...process.env, ...env }; + + return { + plugins: [tsConfigPaths()], + }; +}); diff --git a/app/components/Button/Button.stories.tsx b/app/components/Button/Button.stories.tsx new file mode 100644 index 0000000..50e3540 --- /dev/null +++ b/app/components/Button/Button.stories.tsx @@ -0,0 +1,75 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; + +import Button from "./Button"; + +const meta: Meta = { + title: "Components/Button", + component: Button, + args: { + onClick: fn(), + }, + argTypes: { + onClick: { action: "clicked" }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + args: { + label: "Submit", + primary: true, + onClick: fn(), + loading: false, + loadingText: undefined, + error: undefined, + }, + argTypes: { + onClick: { action: "clicked" }, + }, +}; + +export const Secondary: Story = { + args: { + label: "Cancel", + primary: false, + onClick: fn(), + loading: false, + loadingText: undefined, + error: undefined, + }, + argTypes: { + onClick: { action: "clicked" }, + }, +}; + +export const Loading: Story = { + args: { + label: "Submit", + primary: true, + onClick: fn(), + loading: true, + loadingText: "Submitting...", + error: undefined, + }, + argTypes: { + onClick: { action: "clicked" }, + }, +}; + +export const WithError: Story = { + args: { + label: "Submit", + primary: true, + onClick: fn(), + loading: false, + loadingText: undefined, + error: "Something went wrong", + }, + argTypes: { + onClick: { action: "clicked" }, + }, +}; diff --git a/app/components/Button/Button.test.tsx b/app/components/Button/Button.test.tsx new file mode 100644 index 0000000..749a5bf --- /dev/null +++ b/app/components/Button/Button.test.tsx @@ -0,0 +1,45 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import Button from "./Button"; + +describe(" + {error ?
{error}
: null} + + ); +} diff --git a/app/components/Forms/BottleForm/BottleForm.stories.tsx b/app/components/Forms/BottleForm/BottleForm.stories.tsx new file mode 100644 index 0000000..c49266d --- /dev/null +++ b/app/components/Forms/BottleForm/BottleForm.stories.tsx @@ -0,0 +1,100 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import BottleForm from "./BottleForm"; + +const meta: Meta = { + title: "Components/Forms/BottleForm", + component: BottleForm, +}; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + args: { + inputs: { + name: { + id: "name", + name: "name", + }, + status: { + id: "status", + name: "status", + }, + type: { + id: "type", + name: "type", + }, + distiller: { + id: "distiller", + name: "distiller", + }, + producer: { + id: "producer", + name: "producer", + }, + country: { + id: "country", + name: "country", + }, + region: { + id: "region", + name: "region", + }, + price: { + id: "price", + name: "price", + }, + age: { + id: "age", + name: "age", + }, + year: { + id: "year", + name: "year", + }, + batch: { + id: "batch", + name: "batch", + }, + barrel: { + id: "barrel", + name: "barrel", + }, + alcoholPercent: { + id: "alcoholPercent", + name: "alcoholPercent", + }, + proof: { + id: "proof", + name: "proof", + }, + size: { + id: "size", + name: "size", + }, + color: { + id: "color", + name: "color", + }, + finishing: { + id: "finishing", + name: "finishing", + }, + imageUrl: { + id: "imageUrl", + name: "imageUrl", + }, + openDate: { + id: "openDate", + name: "openDate", + }, + killDate: { + id: "killDate", + name: "killDate", + }, + }, + navigationState: "idle", + }, +}; diff --git a/app/components/Forms/BottleForm/BottleForm.test.tsx b/app/components/Forms/BottleForm/BottleForm.test.tsx new file mode 100644 index 0000000..62eb61f --- /dev/null +++ b/app/components/Forms/BottleForm/BottleForm.test.tsx @@ -0,0 +1,208 @@ +import { screen, render, waitFor } from "@testing-library/react"; +import * as userEvent from "@testing-library/user-event"; +import { select } from "react-select-event"; +import { describe, it, expect } from "vitest"; + +import BottleForm, { Inputs } from "./BottleForm"; + +function setup(jsx: JSX.Element) { + return { + user: userEvent.userEvent.setup(), + ...render(jsx), + payload: { + name: screen.getByLabelText("Name"), + status: screen.getByLabelText("Status"), + type: screen.getByLabelText("Spirit Type"), + distiller: screen.getByLabelText("Distillery"), + producer: screen.getByLabelText("Producer"), + country: screen.getByLabelText("Country of Origin"), + region: screen.getByLabelText("Region"), + price: screen.getByLabelText("Price"), + age: screen.getByLabelText("Age"), + year: screen.getByLabelText("Release Year"), + batch: screen.getByLabelText("Batch"), + barrel: screen.getByLabelText("Barrel #"), + alcoholPercent: screen.getByLabelText("ABV"), + proof: screen.getByLabelText("Proof"), + size: screen.getByLabelText("Bottle Size"), + color: screen.getByLabelText("Color"), + finishing: screen.getByLabelText("Finishing Barrels"), + imageUrl: screen.getByLabelText("Image URL"), + openDate: screen.getByLabelText("Bottle opened on"), + killDate: screen.getByLabelText("Bottle finished on"), + }, + }; +} + +const inputs: Inputs = { + name: { + id: "name", + name: "name", + }, + status: { + id: "status", + name: "status", + }, + type: { + id: "type", + name: "type", + }, + distiller: { + id: "distiller", + name: "distiller", + }, + producer: { + id: "producer", + name: "producer", + }, + country: { + id: "country", + name: "country", + }, + region: { + id: "region", + name: "region", + }, + price: { + id: "price", + name: "price", + }, + age: { + id: "age", + name: "age", + }, + year: { + id: "year", + name: "year", + }, + batch: { + id: "batch", + name: "batch", + }, + barrel: { + id: "barrel", + name: "barrel", + }, + alcoholPercent: { + id: "alcoholPercent", + name: "alcoholPercent", + }, + proof: { + id: "proof", + name: "proof", + }, + size: { + id: "size", + name: "size", + }, + color: { + id: "color", + name: "color", + }, + finishing: { + id: "finishing", + name: "finishing", + }, + imageUrl: { + id: "imageUrl", + name: "imageUrl", + }, + openDate: { + id: "openDate", + name: "openDate", + }, + killDate: { + id: "killDate", + name: "killDate", + }, +}; + +describe("", () => { + it("Renders to the screen", () => { + const { payload } = setup( + , + ); + + expect(payload.name).toBeInTheDocument(); + expect(payload.status).toBeInTheDocument(); + expect(payload.type).toBeInTheDocument(); + expect(payload.distiller).toBeInTheDocument(); + expect(payload.producer).toBeInTheDocument(); + expect(payload.country).toBeInTheDocument(); + expect(payload.region).toBeInTheDocument(); + expect(payload.price).toBeInTheDocument(); + expect(payload.age).toBeInTheDocument(); + expect(payload.year).toBeInTheDocument(); + expect(payload.batch).toBeInTheDocument(); + expect(payload.barrel).toBeInTheDocument(); + expect(payload.alcoholPercent).toBeInTheDocument(); + expect(payload.proof).toBeInTheDocument(); + expect(payload.size).toBeInTheDocument(); + expect(payload.color).toBeInTheDocument(); + expect(payload.finishing).toBeInTheDocument(); + expect(payload.imageUrl).toBeInTheDocument(); + expect(payload.openDate).toBeInTheDocument(); + expect(payload.killDate).toBeInTheDocument(); + }); + it("Successfully submits a full payload", async () => { + const { user, payload } = setup( +
+
+ , + + +
, + ); + + await user.type(screen.getByLabelText("Name"), "Buffalo Trace"); + + await waitFor(async () => + select(screen.getByLabelText("Status"), "Finished"), + ); + await user.type(payload.type, "Bourbon"), + await user.type(payload.distiller, "Buffalo Trace"); + await user.type(payload.producer, "Sazerac"); + await user.type(payload.country, "USA"); + await user.type(payload.region, "Kentucky"); + await user.type(payload.price, "30"); + await user.type(payload.age, "10"); + await user.type(payload.year, "2010"); + await user.type(payload.batch, "1"); + await user.type(payload.barrel, "1"); + await user.type(payload.alcoholPercent, "45"); + await user.type(payload.proof, "90"); + await user.type(payload.size, "750"); + await user.type(payload.color, "Amber"); + await user.type(payload.finishing, "None"); + await user.type(payload.imageUrl, "https://example.com/image.jpg"); + await user.type(payload.openDate, "1/9/2022"); + await user.type(payload.killDate, "6/13/2023"); + + await user.click(screen.getByRole("button", { name: "Submit" })); + + expect( + screen.getByRole("form", { name: "New bottle form" }), + ).toHaveFormValues({ + name: "Buffalo Trace", + status: "FINISHED", + type: "Bourbon", + distiller: "Buffalo Trace", + producer: "Sazerac", + country: "USA", + region: "Kentucky", + price: "30", + age: "10", + year: "2010", + batch: "1", + barrel: "1", + alcoholPercent: "45", + proof: "90", + size: "750", + color: "Amber", + finishing: "None", + imageUrl: "https://example.com/image.jpg", + openDate: "1/9/2022", + killDate: "6/13/2023", + }); + }); +}); diff --git a/app/components/Forms/BottleForm/BottleForm.tsx b/app/components/Forms/BottleForm/BottleForm.tsx index 128de7e..ebe0ffe 100644 --- a/app/components/Forms/BottleForm/BottleForm.tsx +++ b/app/components/Forms/BottleForm/BottleForm.tsx @@ -2,9 +2,8 @@ import { Fieldset, conform } from "@conform-to/react"; import Input from "~/components/Input/Input"; import Status from "~/components/Status/Status"; -import { Options } from "~/types/options"; -type Inputs = Fieldset<{ +export type Inputs = Fieldset<{ name: string; status: "CLOSED" | "OPENED" | "FINISHED"; type: string; @@ -32,21 +31,6 @@ interface BottleFormProps { navigationState: "idle" | "submitting" | "loading"; } -const options: Options = [ - { - value: "CLOSED", - label: "Closed", - }, - { - value: "OPENED", - label: "Opened", - }, - { - value: "FINISHED", - label: "Finished", - }, -]; - export default function BottleForm({ inputs, navigationState, @@ -63,7 +47,7 @@ export default function BottleForm({ navigationState={navigationState} />
- +
- + primary + label="Next" + onClick={() => console.log("Submitting settings...")} + loading={isSubmitting} + loadingText="Submitting..." + /> ); } diff --git a/app/components/Input/Input.stories.tsx b/app/components/Input/Input.stories.tsx new file mode 100644 index 0000000..d68e412 --- /dev/null +++ b/app/components/Input/Input.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import Input from "./Input"; + +const meta: Meta = { + title: "Components/Input", + component: Input, +}; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + args: { + label: "Name", + name: "name", + error: undefined, + navigationState: "idle", + defaultValue: "", + }, +}; + +export const WithDefaultValue: Story = { + args: { + label: "Name", + name: "name", + error: undefined, + navigationState: "idle", + defaultValue: "Stagg Jr.", + }, +}; + +export const WithError: Story = { + args: { + label: "Name", + name: "name", + error: "This field is required.", + navigationState: "idle", + defaultValue: "", + }, +}; diff --git a/app/components/Input/Input.test.tsx b/app/components/Input/Input.test.tsx index 01d1ab1..3e01cf7 100644 --- a/app/components/Input/Input.test.tsx +++ b/app/components/Input/Input.test.tsx @@ -38,4 +38,17 @@ describe("", () => { await user.type(input, "Buffalo Trace"); expect(input).toHaveValue("Buffalo Trace"); }); + it("Shows error message if triggered", () => { + render( + , + ); + + expect(screen.getByText("Error")).toBeInTheDocument(); + }); }); diff --git a/app/components/Input/Input.tsx b/app/components/Input/Input.tsx index 64cf641..6562079 100644 --- a/app/components/Input/Input.tsx +++ b/app/components/Input/Input.tsx @@ -25,7 +25,7 @@ export default function Input({ navigationState, }: InputProps) { return ( -
+