diff --git a/src/features/auth/ui/LoginForm/LoginForm.spec.tsx b/src/features/auth/ui/LoginForm/LoginForm.spec.tsx
new file mode 100644
index 0000000..447259d
--- /dev/null
+++ b/src/features/auth/ui/LoginForm/LoginForm.spec.tsx
@@ -0,0 +1,58 @@
+import { selectAuthData, useAuthStore } from "../../model";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { LoginForm } from "./LoginForm";
+import { MOCK_EMAIL, MOCK_PASSWORD } from "../../api";
+
+describe("LoginForm", () => {
+ afterEach(() => {
+ useAuthStore.getState().reset();
+ vi.restoreAllMocks();
+ });
+
+ it("should render", () => {
+ render();
+
+ expect(screen.getByRole("form")).toBeInTheDocument();
+ });
+
+ it("disables submit button when form is invalid", () => {
+ render();
+ expect(screen.getByRole("button", { name: /login/i })).toBeDisabled();
+ });
+
+ it("enables submit button when form is valid", () => {
+ useAuthStore.getState().setEmail(MOCK_EMAIL);
+ useAuthStore.getState().setPassword(MOCK_PASSWORD);
+ render();
+ expect(screen.getByRole("button", { name: /login/i })).toBeEnabled();
+ });
+
+ it("updates email when user types", async () => {
+ render();
+ const emailInput = screen.getByRole("textbox", { name: /email/i });
+ await userEvent.type(emailInput, MOCK_EMAIL);
+ expect(selectAuthData(useAuthStore.getState()).email).toBe(MOCK_EMAIL);
+ });
+
+ it("updates password when user types", async () => {
+ render();
+ const passwordInput = screen.getByLabelText(/password/i);
+ await userEvent.type(passwordInput, MOCK_PASSWORD);
+ expect(selectAuthData(useAuthStore.getState()).password).toBe(
+ MOCK_PASSWORD,
+ );
+ });
+
+ it("calls login when submit form is valid and user clicks submit", async () => {
+ const loginSpy = vi.spyOn(useAuthStore.getState(), "login");
+
+ useAuthStore.getState().setEmail(MOCK_EMAIL);
+ useAuthStore.getState().setPassword(MOCK_PASSWORD);
+
+ render();
+ const submitButton = screen.getByRole("button", { name: /login/i });
+ await userEvent.click(submitButton);
+ expect(loginSpy).toHaveBeenCalled();
+ });
+});
diff --git a/src/features/auth/ui/LoginForm/LoginForm.stories.ts b/src/features/auth/ui/LoginForm/LoginForm.stories.ts
new file mode 100644
index 0000000..0cbd906
--- /dev/null
+++ b/src/features/auth/ui/LoginForm/LoginForm.stories.ts
@@ -0,0 +1,12 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { LoginForm } from "./LoginForm";
+
+const meta: Meta = {
+ component: LoginForm,
+ title: "features/auth/LoginForm",
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/features/auth/ui/LoginForm/LoginForm.tsx b/src/features/auth/ui/LoginForm/LoginForm.tsx
index a33fead..530f02e 100644
--- a/src/features/auth/ui/LoginForm/LoginForm.tsx
+++ b/src/features/auth/ui/LoginForm/LoginForm.tsx
@@ -1,5 +1,44 @@
+import { selectFormValid, useAuthStore } from "../../model";
+import type { SubmitEvent, ChangeEvent } from "react";
+
+/**
+ * Login form component
+ */
export function LoginForm() {
+ const { formData, setEmail, setPassword, login } = useAuthStore();
+
+ const formValid = useAuthStore(selectFormValid);
+
+ const handleEmailChange = (e: ChangeEvent) => {
+ setEmail(e.target.value);
+ };
+
+ const handlePasswordChange = (e: ChangeEvent) => {
+ setPassword(e.target.value);
+ };
+
+ const handleSubmit = (e: SubmitEvent) => {
+ e.preventDefault();
+ login();
+ };
+
return (
-
+
);
}