From b871bf27de3503d142a114421fd18281d011edb6 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 11:15:23 +0300 Subject: [PATCH 01/12] feat(auth): add formData to the auth store; add corresponding setEmail and setPassword actions --- .../model/stores/authStore/authStore.spec.ts | 14 +++++++++++++ .../auth/model/stores/authStore/authStore.ts | 16 ++++++++++++++- src/features/auth/model/types/store.ts | 20 ++++++++++--------- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/features/auth/model/stores/authStore/authStore.spec.ts b/src/features/auth/model/stores/authStore/authStore.spec.ts index 1398b53..eba0cc9 100644 --- a/src/features/auth/model/stores/authStore/authStore.spec.ts +++ b/src/features/auth/model/stores/authStore/authStore.spec.ts @@ -22,6 +22,20 @@ describe("authStore", () => { }); afterAll(() => server.close()); + describe("setEmail", () => { + it("should set the email in formData", () => { + useAuthStore.getState().setEmail(MOCK_NEW_EMAIL); + expect(useAuthStore.getState().formData.email).toBe(MOCK_NEW_EMAIL); + }); + }); + + describe("setPassword", () => { + it("should set the password in formData", () => { + useAuthStore.getState().setPassword(MOCK_PASSWORD); + expect(useAuthStore.getState().formData.password).toBe(MOCK_PASSWORD); + }); + }); + describe("reset", () => { it("should reset the store to default state", () => { useAuthStore.getState().reset(); diff --git a/src/features/auth/model/stores/authStore/authStore.ts b/src/features/auth/model/stores/authStore/authStore.ts index 6b6d36f..ccda162 100644 --- a/src/features/auth/model/stores/authStore/authStore.ts +++ b/src/features/auth/model/stores/authStore/authStore.ts @@ -5,6 +5,10 @@ import { callApi } from "shared/utils"; import { UNEXPECTED_ERROR_MESSAGE } from "shared/api"; export const defaultStoreState: Readonly = { + formData: { + email: "", + password: "", + }, user: undefined, status: "idle", error: null, @@ -12,7 +16,17 @@ export const defaultStoreState: Readonly = { export const useAuthStore = create()((set) => ({ ...defaultStoreState, - reset: () => set(defaultStoreState), + + setEmail: (email: string) => { + set((state) => ({ formData: { ...state.formData, email } })); + }, + setPassword: (password: string) => { + set((state) => ({ formData: { ...state.formData, password } })); + }, + reset: () => { + set(defaultStoreState); + }, + login: async (loginData) => { set({ status: "loading" }); try { diff --git a/src/features/auth/model/types/store.ts b/src/features/auth/model/types/store.ts index e6e356a..5dc9606 100644 --- a/src/features/auth/model/types/store.ts +++ b/src/features/auth/model/types/store.ts @@ -3,6 +3,10 @@ import type { AuthData, AuthStatus } from "./service"; import type { ApiError } from "shared/utils"; export interface AuthStoreState { + /** + * Form data for login/register forms + */ + formData: AuthData; /** * User's credentials */ @@ -17,16 +21,14 @@ export interface AuthStoreState { error: ApiError | Error | null; } -export type ResetAction = () => void; -export type LoginAction = (data: AuthData) => Promise; -export type RegisterAction = (data: AuthData) => Promise; -export type LogoutAction = () => Promise; - export interface AuthStoreActions { - reset: ResetAction; - login: LoginAction; - register: RegisterAction; - logout: LogoutAction; + setEmail: (email: string) => void; + setPassword: (password: string) => void; + reset: () => void; + + login: (data: AuthData) => Promise; + register: (data: AuthData) => Promise; + logout: () => Promise; } export type AuthStore = AuthStoreState & AuthStoreActions; From 8cea93220b0873f4fff7af4045d4072660b7a3ed Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 13:02:58 +0300 Subject: [PATCH 02/12] feat(auth): create selectors for authStore; cover them with tests --- src/features/auth/model/selectors/index.ts | 2 ++ .../selectAuthData/selectAuthData.spec.ts | 14 +++++++++++ .../selectAuthData/selectAuthData.ts | 7 ++++++ .../selectFormValid/selectFormValid.spec.ts | 25 +++++++++++++++++++ .../selectFormValid/selectFormValid.ts | 4 +++ 5 files changed, 52 insertions(+) create mode 100644 src/features/auth/model/selectors/index.ts create mode 100644 src/features/auth/model/selectors/selectAuthData/selectAuthData.spec.ts create mode 100644 src/features/auth/model/selectors/selectAuthData/selectAuthData.ts create mode 100644 src/features/auth/model/selectors/selectFormValid/selectFormValid.spec.ts create mode 100644 src/features/auth/model/selectors/selectFormValid/selectFormValid.ts diff --git a/src/features/auth/model/selectors/index.ts b/src/features/auth/model/selectors/index.ts new file mode 100644 index 0000000..f81592d --- /dev/null +++ b/src/features/auth/model/selectors/index.ts @@ -0,0 +1,2 @@ +export * from "./selectFormValid/selectFormValid"; +export * from "./selectAuthData/selectAuthData"; diff --git a/src/features/auth/model/selectors/selectAuthData/selectAuthData.spec.ts b/src/features/auth/model/selectors/selectAuthData/selectAuthData.spec.ts new file mode 100644 index 0000000..4fce0b1 --- /dev/null +++ b/src/features/auth/model/selectors/selectAuthData/selectAuthData.spec.ts @@ -0,0 +1,14 @@ +import { useAuthStore } from "../../stores/authStore/authStore"; +import { selectAuthData } from "./selectAuthData"; +import { MOCK_EMAIL, MOCK_PASSWORD } from "../../../api/calls/mocks"; + +describe("selectAuthData", () => { + it("should return the correct auth data", () => { + useAuthStore.getState().setEmail(MOCK_EMAIL); + useAuthStore.getState().setPassword(MOCK_PASSWORD); + expect(selectAuthData(useAuthStore.getState())).toEqual({ + email: MOCK_EMAIL, + password: MOCK_PASSWORD, + }); + }); +}); diff --git a/src/features/auth/model/selectors/selectAuthData/selectAuthData.ts b/src/features/auth/model/selectors/selectAuthData/selectAuthData.ts new file mode 100644 index 0000000..a4a7f02 --- /dev/null +++ b/src/features/auth/model/selectors/selectAuthData/selectAuthData.ts @@ -0,0 +1,7 @@ +import type { AuthData } from "../../types/service"; +import type { AuthStore } from "../../types/store"; + +export const selectAuthData = (state: AuthStore): AuthData => ({ + email: state.formData.email.value, + password: state.formData.password.value, +}); diff --git a/src/features/auth/model/selectors/selectFormValid/selectFormValid.spec.ts b/src/features/auth/model/selectors/selectFormValid/selectFormValid.spec.ts new file mode 100644 index 0000000..f6d85c1 --- /dev/null +++ b/src/features/auth/model/selectors/selectFormValid/selectFormValid.spec.ts @@ -0,0 +1,25 @@ +import { MOCK_EMAIL, MOCK_PASSWORD } from "../../../api/calls/mocks"; +import { useAuthStore } from "../../stores/authStore/authStore"; +import { selectFormValid } from "./selectFormValid"; + +describe("selectFormValid", () => { + afterEach(() => { + useAuthStore.getState().reset(); + }); + + it("should be false when email is invalid", () => { + useAuthStore.getState().setEmail(""); + expect(selectFormValid(useAuthStore.getState())).toBe(false); + }); + + it("should be false when password is invalid", () => { + useAuthStore.getState().setPassword(""); + expect(selectFormValid(useAuthStore.getState())).toBe(false); + }); + + it("should be true when email and password are valid", () => { + useAuthStore.getState().setEmail(MOCK_EMAIL); + useAuthStore.getState().setPassword(MOCK_PASSWORD); + expect(selectFormValid(useAuthStore.getState())).toBe(true); + }); +}); diff --git a/src/features/auth/model/selectors/selectFormValid/selectFormValid.ts b/src/features/auth/model/selectors/selectFormValid/selectFormValid.ts new file mode 100644 index 0000000..05d4c4a --- /dev/null +++ b/src/features/auth/model/selectors/selectFormValid/selectFormValid.ts @@ -0,0 +1,4 @@ +import type { AuthStore } from "../../types/store"; + +export const selectFormValid = (state: AuthStore) => + Object.values(state.formData).every((field) => field.valid); From c378f7c83a85bc2963ad50f0818df5572e79be4f Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 13:04:58 +0300 Subject: [PATCH 03/12] refactor(auth): rewrite store actions, remove arguments, add checks validation and status checks; add test cases --- .../model/stores/authStore/authStore.spec.ts | 106 ++++++++++++++---- .../auth/model/stores/authStore/authStore.ts | 75 +++++++++++-- src/features/auth/model/types/store.ts | 15 ++- 3 files changed, 159 insertions(+), 37 deletions(-) diff --git a/src/features/auth/model/stores/authStore/authStore.spec.ts b/src/features/auth/model/stores/authStore/authStore.spec.ts index eba0cc9..19bcdc0 100644 --- a/src/features/auth/model/stores/authStore/authStore.spec.ts +++ b/src/features/auth/model/stores/authStore/authStore.spec.ts @@ -1,10 +1,5 @@ import { setupServer } from "msw/node"; -import { - loginMock, - registerMock, - logoutMock, - refreshMock, -} from "../../../api/calls"; +import * as apiCalls from "../../../api/calls"; import { defaultStoreState, useAuthStore } from "./authStore"; import { MOCK_EMAIL, @@ -12,7 +7,16 @@ import { MOCK_PASSWORD, } from "../../../api/calls/mocks"; -const server = setupServer(loginMock, registerMock, logoutMock, refreshMock); +const server = setupServer( + apiCalls.loginMock, + apiCalls.registerMock, + apiCalls.logoutMock, + apiCalls.refreshMock, +); + +const loginSpy = vi.spyOn(apiCalls, "login"); +const registerSpy = vi.spyOn(apiCalls, "register"); +const logoutSpy = vi.spyOn(apiCalls, "logout"); describe("authStore", () => { beforeAll(() => server.listen({ onUnhandledRequest: "error" })); @@ -25,14 +29,18 @@ describe("authStore", () => { describe("setEmail", () => { it("should set the email in formData", () => { useAuthStore.getState().setEmail(MOCK_NEW_EMAIL); - expect(useAuthStore.getState().formData.email).toBe(MOCK_NEW_EMAIL); + expect(useAuthStore.getState().formData.email).toMatchObject({ + value: MOCK_NEW_EMAIL, + }); }); }); describe("setPassword", () => { it("should set the password in formData", () => { useAuthStore.getState().setPassword(MOCK_PASSWORD); - expect(useAuthStore.getState().formData.password).toBe(MOCK_PASSWORD); + expect(useAuthStore.getState().formData.password).toMatchObject({ + value: MOCK_PASSWORD, + }); }); }); @@ -44,10 +52,33 @@ describe("authStore", () => { }); describe("login", () => { + it("should not do api call if status is loading", async () => { + useAuthStore.getState().setEmail(MOCK_EMAIL); + useAuthStore.getState().setPassword(MOCK_PASSWORD); + useAuthStore.setState({ status: "loading" }); + + await useAuthStore.getState().login(); + + expect(loginSpy).not.toHaveBeenCalled(); + }); + + it("should not do api call if form is invalid", async () => { + useAuthStore.getState().setEmail(""); + useAuthStore.getState().setPassword(""); + + await useAuthStore.getState().login(); + + const { status } = useAuthStore.getState(); + + expect(loginSpy).not.toHaveBeenCalled(); + expect(status).toBe("idle"); + }); + it("should set access token, user data, and update status after successful login", async () => { - await useAuthStore - .getState() - .login({ email: MOCK_EMAIL, password: MOCK_PASSWORD }); + useAuthStore.getState().setEmail(MOCK_EMAIL); + useAuthStore.getState().setPassword(MOCK_PASSWORD); + + await useAuthStore.getState().login(); const { user, status, error } = useAuthStore.getState(); @@ -57,9 +88,10 @@ describe("authStore", () => { }); it("should set error and update status if login fails", async () => { - await useAuthStore - .getState() - .login({ email: "wrong@test.com", password: "wrongPassword" }); + useAuthStore.getState().setEmail("wrong@test.com"); + useAuthStore.getState().setPassword("wrongPassword"); + + await useAuthStore.getState().login(); const { status, error } = useAuthStore.getState(); @@ -69,10 +101,33 @@ describe("authStore", () => { }); describe("register", () => { + it("should not do api call if status is loading", async () => { + useAuthStore.getState().setEmail(MOCK_EMAIL); + useAuthStore.getState().setPassword(MOCK_PASSWORD); + useAuthStore.setState({ status: "loading" }); + + await useAuthStore.getState().register(); + + expect(registerSpy).not.toHaveBeenCalled(); + }); + + it("should not do api call if form is invalid", async () => { + useAuthStore.getState().setEmail(""); + useAuthStore.getState().setPassword(""); + + await useAuthStore.getState().register(); + + const { status } = useAuthStore.getState(); + + expect(registerSpy).not.toHaveBeenCalled(); + expect(status).toBe("idle"); + }); + it("should set access token, user data, and update status after successful registration", async () => { - await useAuthStore - .getState() - .register({ email: MOCK_NEW_EMAIL, password: MOCK_PASSWORD }); + useAuthStore.getState().setEmail(MOCK_NEW_EMAIL); + useAuthStore.getState().setPassword(MOCK_PASSWORD); + + await useAuthStore.getState().register(); const { user, status, error } = useAuthStore.getState(); @@ -82,9 +137,10 @@ describe("authStore", () => { }); it("should set error and update status if registration fails", async () => { - await useAuthStore - .getState() - .register({ email: MOCK_EMAIL, password: MOCK_PASSWORD }); + useAuthStore.getState().setEmail(MOCK_EMAIL); + useAuthStore.getState().setPassword(MOCK_PASSWORD); + + await useAuthStore.getState().register(); const { status, error } = useAuthStore.getState(); @@ -94,6 +150,14 @@ describe("authStore", () => { }); describe("logout", () => { + it("should not do api call if status is loading", async () => { + useAuthStore.setState({ status: "loading" }); + + await useAuthStore.getState().logout(); + + expect(logoutSpy).not.toHaveBeenCalled(); + }); + it("should clear access token, user data, and update status after logout", async () => { await useAuthStore.getState().logout(); diff --git a/src/features/auth/model/stores/authStore/authStore.ts b/src/features/auth/model/stores/authStore/authStore.ts index ccda162..152ee4e 100644 --- a/src/features/auth/model/stores/authStore/authStore.ts +++ b/src/features/auth/model/stores/authStore/authStore.ts @@ -3,33 +3,66 @@ import type { AuthStore, AuthStoreState } from "../../types/store"; import { login, logout, register } from "../../../api"; import { callApi } from "shared/utils"; import { UNEXPECTED_ERROR_MESSAGE } from "shared/api"; +import { selectAuthData, selectFormValid } from "../../selectors"; export const defaultStoreState: Readonly = { formData: { - email: "", - password: "", + email: { value: "", valid: false }, + password: { value: "", valid: false }, }, user: undefined, status: "idle", error: null, }; -export const useAuthStore = create()((set) => ({ +function validateEmail(email: string): boolean { + return Boolean(email); +} + +function validatePassword(password: string): boolean { + return Boolean(password); +} + +export const useAuthStore = create()((set, get) => ({ ...defaultStoreState, setEmail: (email: string) => { - set((state) => ({ formData: { ...state.formData, email } })); + const isValid = validateEmail(email); + set((state) => ({ + formData: { ...state.formData, email: { value: email, valid: isValid } }, + })); }, setPassword: (password: string) => { - set((state) => ({ formData: { ...state.formData, password } })); + const isValid = validatePassword(password); + set((state) => ({ + formData: { + ...state.formData, + password: { value: password, valid: isValid }, + }, + })); }, reset: () => { set(defaultStoreState); }, - login: async (loginData) => { + login: async () => { + const { status } = get(); + + if (status === "loading") { + return; + } + set({ status: "loading" }); + + const formValid = selectFormValid(get()); + + if (!formValid) { + set({ status: "idle" }); + return; + } + try { + const loginData = selectAuthData(get()); const [responseData, loginError] = await callApi(() => login(loginData)); if (loginError) { @@ -42,8 +75,6 @@ export const useAuthStore = create()((set) => ({ user: responseData?.user, error: null, }); - - // useTokenStore.setState({ accessToken: responseData?.accessToken }); } catch (err) { console.error(err); set({ @@ -52,8 +83,24 @@ export const useAuthStore = create()((set) => ({ }); } }, - register: async (registerData) => { + register: async () => { + const { status } = get(); + + if (status === "loading") { + return; + } + + set({ status: "loading" }); + + const formValid = selectFormValid(get()); + + if (!formValid) { + set({ status: "idle" }); + return; + } + try { + const registerData = selectAuthData(get()); const [responseData, registerError] = await callApi(() => register(registerData), ); @@ -68,8 +115,6 @@ export const useAuthStore = create()((set) => ({ user: responseData?.user, error: null, }); - - // useTokenStore.setState({ accessToken: responseData?.accessToken }); } catch (err) { console.error(err); set({ @@ -79,12 +124,18 @@ export const useAuthStore = create()((set) => ({ } }, logout: async () => { + const prevStatus = get().status; + + if (prevStatus === "loading") { + return; + } + set({ status: "loading" }); try { const [, logoutError] = await callApi(() => logout()); if (logoutError) { - set({ error: logoutError }); + set({ error: logoutError, status: prevStatus }); return; } diff --git a/src/features/auth/model/types/store.ts b/src/features/auth/model/types/store.ts index 5dc9606..d84b8ba 100644 --- a/src/features/auth/model/types/store.ts +++ b/src/features/auth/model/types/store.ts @@ -2,13 +2,20 @@ import type { User } from "entities/User"; import type { AuthData, AuthStatus } from "./service"; import type { ApiError } from "shared/utils"; +export type AuthFormData = { + [K in keyof AuthData]: { + value: AuthData[K]; + valid: boolean; + }; +}; + export interface AuthStoreState { /** * Form data for login/register forms */ - formData: AuthData; + formData: AuthFormData; /** - * User's credentials + * Current user */ user?: User; /** @@ -26,8 +33,8 @@ export interface AuthStoreActions { setPassword: (password: string) => void; reset: () => void; - login: (data: AuthData) => Promise; - register: (data: AuthData) => Promise; + login: () => Promise; + register: () => Promise; logout: () => Promise; } From 1f20f11852da8d92cab6b7d6ec594886564d457c Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 19:28:20 +0300 Subject: [PATCH 04/12] chore: install storybook --- eslint.config.js | 30 +- package.json | 15 +- vitest.config.ts | 57 ++- vitest.shims.d.ts | 1 + yarn.lock | 946 +++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 1004 insertions(+), 45 deletions(-) create mode 100644 vitest.shims.d.ts diff --git a/eslint.config.js b/eslint.config.js index 5e6b472..5964a38 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,6 @@ +// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import storybook from "eslint-plugin-storybook"; + import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' @@ -5,19 +8,16 @@ import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' import { defineConfig, globalIgnores } from 'eslint/config' -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, +export default defineConfig([globalIgnores(['dist']), { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, }, -]) +}, ...storybook.configs["flat/recommended"]]) diff --git a/package.json b/package.json index 8c6e63a..fd97c60 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "preview": "vite preview", "test:unit": "vitest", "test:run": "vitest run", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "dependencies": { "ky": "^1.14.3", @@ -19,8 +21,13 @@ "zustand": "^5.0.12" }, "devDependencies": { + "@chromatic-com/storybook": "^5.0.2", "@eslint/js": "^9.39.1", "@module-federation/vite": "^1.12.3", + "@storybook/addon-a11y": "^10.3.3", + "@storybook/addon-docs": "^10.3.3", + "@storybook/addon-vitest": "^10.3.3", + "@storybook/react-vite": "^10.3.3", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -29,14 +36,18 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.4", + "@vitest/browser-playwright": "4.1.0", "@vitest/coverage-v8": "^4.1.0", "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-storybook": "^10.3.3", "globals": "^16.5.0", "jsdom": "^29.0.0", "msw": "^2.12.11", + "playwright": "^1.58.2", + "storybook": "^10.3.3", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", "vite": "^7.3.1", @@ -47,4 +58,4 @@ "public" ] } -} \ No newline at end of file +} diff --git a/vitest.config.ts b/vitest.config.ts index 152a536..b6d7a2a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,32 +1,59 @@ import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; import path from "path"; +import { fileURLToPath } from 'node:url'; +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +import { playwright } from '@vitest/browser-playwright'; +const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); +// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon export default defineConfig({ - plugins: [ - react({ - babel: { - plugins: [["babel-plugin-react-compiler", {}]], - }, - }), - ], + plugins: [react({ + babel: { + plugins: [["babel-plugin-react-compiler", {}]] + } + })], resolve: { alias: { shared: path.resolve(__dirname, "src/shared"), entities: path.resolve(__dirname, "src/entities"), features: path.resolve(__dirname, "src/features"), widgets: path.resolve(__dirname, "src/widgets"), - app: path.resolve(__dirname, "src/app"), - }, + app: path.resolve(__dirname, "src/app") + } }, test: { - globals: true, - environment: "jsdom", - setupFiles: ["./src/shared/config/test/setup.ts"], coverage: { provider: "v8", reporter: ["text", "json", "html"], - exclude: ["node_modules/", "src/test/"], + exclude: ["node_modules/", "src/test/"] }, - }, -}); + projects: [{ + extends: true, + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./src/shared/config/test/setup.ts"] + } + }, { + extends: true, + plugins: [ + // The plugin will run tests for the stories defined in your Storybook config + // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest + storybookTest({ + configDir: path.join(dirname, '.storybook') + })], + test: { + name: 'storybook', + browser: { + enabled: true, + headless: true, + provider: playwright({}), + instances: [{ + browser: 'chromium' + }] + } + } + }] + } +}); \ No newline at end of file diff --git a/vitest.shims.d.ts b/vitest.shims.d.ts new file mode 100644 index 0000000..7782f28 --- /dev/null +++ b/vitest.shims.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index da0ab14..2bec5db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,7 +63,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.24.4, @babel/core@npm:^7.29.0": +"@babel/core@npm:^7.24.4, @babel/core@npm:^7.28.0, @babel/core@npm:^7.29.0": version: 7.29.0 resolution: "@babel/core@npm:7.29.0" dependencies: @@ -231,7 +231,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": +"@babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": version: 7.29.0 resolution: "@babel/traverse@npm:7.29.0" dependencies: @@ -263,6 +263,13 @@ __metadata: languageName: node linkType: hard +"@blazediff/core@npm:1.9.1": + version: 1.9.1 + resolution: "@blazediff/core@npm:1.9.1" + checksum: 10c0/fd45cdd0544002341d74831a179ef693a81414abd348c1ff0c01086c0ea03f5e5ee284c4e16c2e6fb3670c265f90a3d85752b9360320efa9a835928e604dae77 + languageName: node + linkType: hard + "@bramus/specificity@npm:^2.4.2": version: 2.4.2 resolution: "@bramus/specificity@npm:2.4.2" @@ -274,6 +281,21 @@ __metadata: languageName: node linkType: hard +"@chromatic-com/storybook@npm:^5.0.2": + version: 5.0.2 + resolution: "@chromatic-com/storybook@npm:5.0.2" + dependencies: + "@neoconfetti/react": "npm:^1.0.0" + chromatic: "npm:^13.3.4" + filesize: "npm:^10.0.12" + jsonfile: "npm:^6.1.0" + strip-ansi: "npm:^7.1.0" + peerDependencies: + storybook: ^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 + checksum: 10c0/f888eb2518cd264d1c9b4199c2d44b60fbbfead7344231b97a0259d0461559cbe7bc02c32e9ffc0ee37f0226a8cdf7c2e4dd3464bc7d1026fee5e4cf9f04f1de + languageName: node + linkType: hard + "@csstools/color-helpers@npm:^6.0.2": version: 6.0.2 resolution: "@csstools/color-helpers@npm:6.0.2" @@ -753,6 +775,22 @@ __metadata: languageName: node linkType: hard +"@joshwooding/vite-plugin-react-docgen-typescript@npm:^0.6.4": + version: 0.6.4 + resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.6.4" + dependencies: + glob: "npm:^13.0.1" + react-docgen-typescript: "npm:^2.2.2" + peerDependencies: + typescript: ">= 4.3.x" + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/73149b2d41d5b8eff7dfe4d037a6903fe4123ae46f3928d88535020539f44159c4ea1b342e6a77d4c14219f2f743fea0ef96e81279cce8b6d247dc4d582e27ed + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.13 resolution: "@jridgewell/gen-mapping@npm:0.3.13" @@ -797,6 +835,18 @@ __metadata: languageName: node linkType: hard +"@mdx-js/react@npm:^3.0.0": + version: 3.1.1 + resolution: "@mdx-js/react@npm:3.1.1" + dependencies: + "@types/mdx": "npm:^2.0.0" + peerDependencies: + "@types/react": ">=16" + react: ">=16" + checksum: 10c0/34ca98bc2a0f969894ea144dc5c8a5294690505458cd24965cd9be854d779c193ad9192bf9143c4c18438fafd1902e100d99067e045c69319288562d497558c6 + languageName: node + linkType: hard + "@module-federation/dts-plugin@npm:2.0.1": version: 2.0.1 resolution: "@module-federation/dts-plugin@npm:2.0.1" @@ -927,6 +977,13 @@ __metadata: languageName: node linkType: hard +"@neoconfetti/react@npm:^1.0.0": + version: 1.0.0 + resolution: "@neoconfetti/react@npm:1.0.0" + checksum: 10c0/dfa487965b69f88b39562ccd910114cd68b00a90c7eb79cfb1a483c7ac717b720f9f095e5aea13cef8a9b9bea05533d380ddff5e44d3bc3f7dc4d5c66716765c + languageName: node + linkType: hard + "@npmcli/agent@npm:^4.0.0": version: 4.0.0 resolution: "@npmcli/agent@npm:4.0.0" @@ -987,6 +1044,13 @@ __metadata: languageName: node linkType: hard +"@polka/url@npm:^1.0.0-next.24": + version: 1.0.0-next.29 + resolution: "@polka/url@npm:1.0.0-next.29" + checksum: 10c0/0d58e081844095cb029d3c19a659bfefd09d5d51a2f791bc61eba7ea826f13d6ee204a8a448c2f5a855c17df07b37517373ff916dd05801063c0568ae9937684 + languageName: node + linkType: hard + "@rolldown/binding-android-arm64@npm:1.0.0-rc.9": version: 1.0.0-rc.9 resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.9" @@ -1108,7 +1172,7 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^5.3.0": +"@rollup/pluginutils@npm:^5.0.2, @rollup/pluginutils@npm:^5.3.0": version: 5.3.0 resolution: "@rollup/pluginutils@npm:5.3.0" dependencies: @@ -1306,6 +1370,167 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-a11y@npm:^10.3.3": + version: 10.3.3 + resolution: "@storybook/addon-a11y@npm:10.3.3" + dependencies: + "@storybook/global": "npm:^5.0.0" + axe-core: "npm:^4.2.0" + peerDependencies: + storybook: ^10.3.3 + checksum: 10c0/da83678c1fc351a3893bab7c4d04a81b11aeeb51112b03cff5c681fd5951b7c12f469410369eb0e02e7a91ce732b4f297077136855a73cdf5dd8ab3735dab3b6 + languageName: node + linkType: hard + +"@storybook/addon-docs@npm:^10.3.3": + version: 10.3.3 + resolution: "@storybook/addon-docs@npm:10.3.3" + dependencies: + "@mdx-js/react": "npm:^3.0.0" + "@storybook/csf-plugin": "npm:10.3.3" + "@storybook/icons": "npm:^2.0.1" + "@storybook/react-dom-shim": "npm:10.3.3" + react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + ts-dedent: "npm:^2.0.0" + peerDependencies: + storybook: ^10.3.3 + checksum: 10c0/19a98f3e8fcf97d35bb25f6cda49708e56006e445d9f04cd80eb697ee452c158203af1f4f3e71358e47a2e257d7fdb85c29ece5f4b36f71dff95070ca4a85af2 + languageName: node + linkType: hard + +"@storybook/addon-vitest@npm:^10.3.3": + version: 10.3.3 + resolution: "@storybook/addon-vitest@npm:10.3.3" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@storybook/icons": "npm:^2.0.1" + peerDependencies: + "@vitest/browser": ^3.0.0 || ^4.0.0 + "@vitest/browser-playwright": ^4.0.0 + "@vitest/runner": ^3.0.0 || ^4.0.0 + storybook: ^10.3.3 + vitest: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + "@vitest/browser": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/runner": + optional: true + vitest: + optional: true + checksum: 10c0/1691bbe974b55510eb1b0d50a542322b79302e42789b35ecdda3fdcd8a289693c681c2a2fcf9c219c63a6d1112433f1f13305bd24143d2183e8d7dd53b65d560 + languageName: node + linkType: hard + +"@storybook/builder-vite@npm:10.3.3": + version: 10.3.3 + resolution: "@storybook/builder-vite@npm:10.3.3" + dependencies: + "@storybook/csf-plugin": "npm:10.3.3" + ts-dedent: "npm:^2.0.0" + peerDependencies: + storybook: ^10.3.3 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/90b002777ff4b0b31ea4bc8d4f6e13f4d4c35a51c2bad7cf0b2e0a3a2f4ec3aa387f87ed174f7589d29842564f61346415dc0c919819e9ab45827c2c0f6141f2 + languageName: node + linkType: hard + +"@storybook/csf-plugin@npm:10.3.3": + version: 10.3.3 + resolution: "@storybook/csf-plugin@npm:10.3.3" + dependencies: + unplugin: "npm:^2.3.5" + peerDependencies: + esbuild: "*" + rollup: "*" + storybook: ^10.3.3 + vite: "*" + webpack: "*" + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + checksum: 10c0/62d52c50555ca0f18907962179aa90287e6b95ba6b31cbbeb071842f1580491ff8578cc628f9fd1809a0ef48e2b23164657204c2de16a3f7c9830c4b69c822aa + languageName: node + linkType: hard + +"@storybook/global@npm:^5.0.0": + version: 5.0.0 + resolution: "@storybook/global@npm:5.0.0" + checksum: 10c0/8f1b61dcdd3a89584540896e659af2ecc700bc740c16909a7be24ac19127ea213324de144a141f7caf8affaed017d064fea0618d453afbe027cf60f54b4a6d0b + languageName: node + linkType: hard + +"@storybook/icons@npm:^2.0.1": + version: 2.0.1 + resolution: "@storybook/icons@npm:2.0.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/df2bbf1a5b50f12ab1bf78cae6de4dbf7c49df0e3a5f845553b51b20adbe8386a09fd172ea60342379f9284bb528cba2d0e2659cae6eb8d015cf92c8b32f1222 + languageName: node + linkType: hard + +"@storybook/react-dom-shim@npm:10.3.3": + version: 10.3.3 + resolution: "@storybook/react-dom-shim@npm:10.3.3" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.3 + checksum: 10c0/d4018e1e2acf64d521a13b2190d263b2e873ac65172facda7e443716ede593195e21bb9e0cd288e785a25a5973527813a5ccdb069881a2bc22e490342237d026 + languageName: node + linkType: hard + +"@storybook/react-vite@npm:^10.3.3": + version: 10.3.3 + resolution: "@storybook/react-vite@npm:10.3.3" + dependencies: + "@joshwooding/vite-plugin-react-docgen-typescript": "npm:^0.6.4" + "@rollup/pluginutils": "npm:^5.0.2" + "@storybook/builder-vite": "npm:10.3.3" + "@storybook/react": "npm:10.3.3" + empathic: "npm:^2.0.0" + magic-string: "npm:^0.30.0" + react-docgen: "npm:^8.0.0" + resolve: "npm:^1.22.8" + tsconfig-paths: "npm:^4.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.3 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/6c044a398201ee35d973269c8d47def841caba628b20df95c316d0723a02b798167366de26281ce2c934a844d69c5a51f1e1bc6d11d7ee20219e3cd59d3c9343 + languageName: node + linkType: hard + +"@storybook/react@npm:10.3.3": + version: 10.3.3 + resolution: "@storybook/react@npm:10.3.3" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@storybook/react-dom-shim": "npm:10.3.3" + react-docgen: "npm:^8.0.2" + react-docgen-typescript: "npm:^2.2.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.3 + typescript: ">= 4.9.x" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/a6c36e4e14685348faf50b74de43603d23e0d18aaa21d16e91f6e0b274b1ab689033a8a42b424d776dcdfefa744a6d919a2f509dae8e0520a19b0b20503a1fa7 + languageName: node + linkType: hard + "@testing-library/dom@npm:^10.4.1": version: 10.4.1 resolution: "@testing-library/dom@npm:10.4.1" @@ -1413,7 +1638,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__traverse@npm:*": +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.20.7": version: 7.28.0 resolution: "@types/babel__traverse@npm:7.28.0" dependencies: @@ -1439,6 +1664,13 @@ __metadata: languageName: node linkType: hard +"@types/doctrine@npm:^0.0.9": + version: 0.0.9 + resolution: "@types/doctrine@npm:0.0.9" + checksum: 10c0/cdaca493f13c321cf0cacd1973efc0ae74569633145d9e6fc1128f32217a6968c33bea1f858275239fe90c98f3be57ec8f452b416a9ff48b8e8c1098b20fa51c + languageName: node + linkType: hard + "@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -1453,6 +1685,13 @@ __metadata: languageName: node linkType: hard +"@types/mdx@npm:^2.0.0": + version: 2.0.13 + resolution: "@types/mdx@npm:2.0.13" + checksum: 10c0/5edf1099505ac568da55f9ae8a93e7e314e8cbc13d3445d0be61b75941226b005e1390d9b95caecf5dcb00c9d1bab2f1f60f6ff9876dc091a48b547495007720 + languageName: node + linkType: hard + "@types/node@npm:^24.10.1": version: 24.12.0 resolution: "@types/node@npm:24.12.0" @@ -1480,6 +1719,13 @@ __metadata: languageName: node linkType: hard +"@types/resolve@npm:^1.20.2": + version: 1.20.6 + resolution: "@types/resolve@npm:1.20.6" + checksum: 10c0/a9b0549d816ff2c353077365d865a33655a141d066d0f5a3ba6fd4b28bc2f4188a510079f7c1f715b3e7af505a27374adce2a5140a3ece2a059aab3d6e1a4244 + languageName: node + linkType: hard + "@types/statuses@npm:^2.0.6": version: 2.0.6 resolution: "@types/statuses@npm:2.0.6" @@ -1536,6 +1782,19 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/project-service@npm:8.57.2": + version: 8.57.2 + resolution: "@typescript-eslint/project-service@npm:8.57.2" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.57.2" + "@typescript-eslint/types": "npm:^8.57.2" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/f84e3165b0a214318d4bc119018b87c044170d7638945e84bd4cee2d752b62c1797ce722ca1161cd06f48512d0115ef75500e6c8fc01005ad4bb39fb48dd77bf + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.57.0": version: 8.57.0 resolution: "@typescript-eslint/scope-manager@npm:8.57.0" @@ -1546,6 +1805,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.57.2": + version: 8.57.2 + resolution: "@typescript-eslint/scope-manager@npm:8.57.2" + dependencies: + "@typescript-eslint/types": "npm:8.57.2" + "@typescript-eslint/visitor-keys": "npm:8.57.2" + checksum: 10c0/532b1a97a5c2fce51400fa1a94e09615b4df84ce1f2d107206a3f3935074cada396a3e30f155582a698981832868e1afea1641ff779ad9456fdc94169b7def64 + languageName: node + linkType: hard + "@typescript-eslint/tsconfig-utils@npm:8.57.0, @typescript-eslint/tsconfig-utils@npm:^8.57.0": version: 8.57.0 resolution: "@typescript-eslint/tsconfig-utils@npm:8.57.0" @@ -1555,6 +1824,15 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/tsconfig-utils@npm:8.57.2, @typescript-eslint/tsconfig-utils@npm:^8.57.2": + version: 8.57.2 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.57.2" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/199dad2d96efc88ce94f5f3e12e97205537bf7a7152e56ef1d84dfbe7bd1babebea9b9f396c01b6c447505a4eb02c1cbbd2c28828c587b51b41b15d017a11d2f + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.57.0": version: 8.57.0 resolution: "@typescript-eslint/type-utils@npm:8.57.0" @@ -1578,6 +1856,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.57.2, @typescript-eslint/types@npm:^8.57.2": + version: 8.57.2 + resolution: "@typescript-eslint/types@npm:8.57.2" + checksum: 10c0/3cd87dd77d28b3ac2fed56a17909b0d11633628d4d733aa148dfd7af72e2cc3ec0e6114b72fac0ff538e8a47e907b4b10dab4095170ae1bd73719ef0b8eaf2e7 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.57.0": version: 8.57.0 resolution: "@typescript-eslint/typescript-estree@npm:8.57.0" @@ -1597,6 +1882,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.57.2": + version: 8.57.2 + resolution: "@typescript-eslint/typescript-estree@npm:8.57.2" + dependencies: + "@typescript-eslint/project-service": "npm:8.57.2" + "@typescript-eslint/tsconfig-utils": "npm:8.57.2" + "@typescript-eslint/types": "npm:8.57.2" + "@typescript-eslint/visitor-keys": "npm:8.57.2" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.4.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/2c5d143f0abbafd07a45f0b956aab5d6487b27f74fe93bee93e0a3f8edc8913f1522faf8d7d5215f3809a8d12f5729910ea522156552f2481b66e6d05ab311ae + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.57.0": version: 8.57.0 resolution: "@typescript-eslint/utils@npm:8.57.0" @@ -1612,6 +1916,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^8.48.0": + version: 8.57.2 + resolution: "@typescript-eslint/utils@npm:8.57.2" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.57.2" + "@typescript-eslint/types": "npm:8.57.2" + "@typescript-eslint/typescript-estree": "npm:8.57.2" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/5771f3d4206004cc817a6556a472926b4c1c885dc448049c10ffab1d5aac7bd59450a391fb57ce8ef31a8367e9c8ddb3bc9370c4e83fc8b61f50fd5189390e8f + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.57.0": version: 8.57.0 resolution: "@typescript-eslint/visitor-keys@npm:8.57.0" @@ -1622,6 +1941,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.57.2": + version: 8.57.2 + resolution: "@typescript-eslint/visitor-keys@npm:8.57.2" + dependencies: + "@typescript-eslint/types": "npm:8.57.2" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10c0/8ceb8c228bf97b3e4b343bf6e42a91998d2522f459eb6b53c6bfad4898a9df74295660893dee6b698bdbbda537e968bfc13a3c56fc341089ebfba13db766a574 + languageName: node + linkType: hard + "@vitejs/plugin-react@npm:^5.1.4": version: 5.2.0 resolution: "@vitejs/plugin-react@npm:5.2.0" @@ -1638,6 +1967,41 @@ __metadata: languageName: node linkType: hard +"@vitest/browser-playwright@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/browser-playwright@npm:4.1.0" + dependencies: + "@vitest/browser": "npm:4.1.0" + "@vitest/mocker": "npm:4.1.0" + tinyrainbow: "npm:^3.0.3" + peerDependencies: + playwright: "*" + vitest: 4.1.0 + peerDependenciesMeta: + playwright: + optional: false + checksum: 10c0/af2f6fc36eb56e3c1ac6e31b0ab2a2f4ca0bda86a306d0991b2f01047213fb191339b35775103af11ce1ef323ec72432eebe4bfeccd744d5e7c658716f1b985a + languageName: node + linkType: hard + +"@vitest/browser@npm:4.1.0": + version: 4.1.0 + resolution: "@vitest/browser@npm:4.1.0" + dependencies: + "@blazediff/core": "npm:1.9.1" + "@vitest/mocker": "npm:4.1.0" + "@vitest/utils": "npm:4.1.0" + magic-string: "npm:^0.30.21" + pngjs: "npm:^7.0.0" + sirv: "npm:^3.0.2" + tinyrainbow: "npm:^3.0.3" + ws: "npm:^8.19.0" + peerDependencies: + vitest: 4.1.0 + checksum: 10c0/33b35cea63f392b6afafb6636bebe7ff0d234b1c120ec74a97462c7a7cbdbc67f415a5f0f95651f4074d53bfe12d4ff3ae8f16ba79045226df6365c77f950e18 + languageName: node + linkType: hard + "@vitest/coverage-v8@npm:^4.1.0": version: 4.1.0 resolution: "@vitest/coverage-v8@npm:4.1.0" @@ -1662,6 +2026,19 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/expect@npm:3.2.4" + dependencies: + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:3.2.4" + "@vitest/utils": "npm:3.2.4" + chai: "npm:^5.2.0" + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/7586104e3fd31dbe1e6ecaafb9a70131e4197dce2940f727b6a84131eee3decac7b10f9c7c72fa5edbdb68b6f854353bd4c0fa84779e274207fb7379563b10db + languageName: node + linkType: hard + "@vitest/expect@npm:4.1.0": version: 4.1.0 resolution: "@vitest/expect@npm:4.1.0" @@ -1695,6 +2072,15 @@ __metadata: languageName: node linkType: hard +"@vitest/pretty-format@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/pretty-format@npm:3.2.4" + dependencies: + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/5ad7d4278e067390d7d633e307fee8103958806a419ca380aec0e33fae71b44a64415f7a9b4bc11635d3c13d4a9186111c581d3cef9c65cc317e68f077456887 + languageName: node + linkType: hard + "@vitest/pretty-format@npm:4.1.0": version: 4.1.0 resolution: "@vitest/pretty-format@npm:4.1.0" @@ -1726,6 +2112,15 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/spy@npm:3.2.4" + dependencies: + tinyspy: "npm:^4.0.3" + checksum: 10c0/6ebf0b4697dc238476d6b6a60c76ba9eb1dd8167a307e30f08f64149612fd50227682b876420e4c2e09a76334e73f72e3ebf0e350714dc22474258292e202024 + languageName: node + linkType: hard + "@vitest/spy@npm:4.1.0": version: 4.1.0 resolution: "@vitest/spy@npm:4.1.0" @@ -1733,6 +2128,17 @@ __metadata: languageName: node linkType: hard +"@vitest/utils@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/utils@npm:3.2.4" + dependencies: + "@vitest/pretty-format": "npm:3.2.4" + loupe: "npm:^3.1.4" + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/024a9b8c8bcc12cf40183c246c244b52ecff861c6deb3477cbf487ac8781ad44c68a9c5fd69f8c1361878e55b97c10d99d511f2597f1f7244b5e5101d028ba64 + languageName: node + linkType: hard + "@vitest/utils@npm:4.1.0": version: 4.1.0 resolution: "@vitest/utils@npm:4.1.0" @@ -1819,6 +2225,13 @@ __metadata: languageName: node linkType: hard +"ansi-regex@npm:^6.2.2": + version: 6.2.2 + resolution: "ansi-regex@npm:6.2.2" + checksum: 10c0/05d4acb1d2f59ab2cf4b794339c7b168890d44dda4bf0ce01152a8da0213aca207802f930442ce8cd22d7a92f44907664aac6508904e75e038fa944d2601b30f + languageName: node + linkType: hard + "ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" @@ -1865,6 +2278,15 @@ __metadata: languageName: node linkType: hard +"ast-types@npm:^0.16.1": + version: 0.16.1 + resolution: "ast-types@npm:0.16.1" + dependencies: + tslib: "npm:^2.0.1" + checksum: 10c0/abcc49e42eb921a7ebc013d5bec1154651fb6dbc3f497541d488859e681256901b2990b954d530ba0da4d0851271d484f7057d5eff5e07cb73e8b10909f711bf + languageName: node + linkType: hard + "ast-v8-to-istanbul@npm:^1.0.0": version: 1.0.0 resolution: "ast-v8-to-istanbul@npm:1.0.0" @@ -1908,8 +2330,13 @@ __metadata: version: 0.0.0-use.local resolution: "auth-react@workspace:." dependencies: + "@chromatic-com/storybook": "npm:^5.0.2" "@eslint/js": "npm:^9.39.1" "@module-federation/vite": "npm:^1.12.3" + "@storybook/addon-a11y": "npm:^10.3.3" + "@storybook/addon-docs": "npm:^10.3.3" + "@storybook/addon-vitest": "npm:^10.3.3" + "@storybook/react-vite": "npm:^10.3.3" "@testing-library/dom": "npm:^10.4.1" "@testing-library/jest-dom": "npm:^6.9.1" "@testing-library/react": "npm:^16.3.2" @@ -1918,17 +2345,21 @@ __metadata: "@types/react": "npm:^19.2.7" "@types/react-dom": "npm:^19.2.3" "@vitejs/plugin-react": "npm:^5.1.4" + "@vitest/browser-playwright": "npm:4.1.0" "@vitest/coverage-v8": "npm:^4.1.0" babel-plugin-react-compiler: "npm:^1.0.0" eslint: "npm:^9.39.1" eslint-plugin-react-hooks: "npm:^7.0.1" eslint-plugin-react-refresh: "npm:^0.4.24" + eslint-plugin-storybook: "npm:^10.3.3" globals: "npm:^16.5.0" jsdom: "npm:^29.0.0" ky: "npm:^1.14.3" msw: "npm:^2.12.11" + playwright: "npm:^1.58.2" react: "npm:^19.2.0" react-dom: "npm:^19.2.0" + storybook: "npm:^10.3.3" typescript: "npm:~5.9.3" typescript-eslint: "npm:^8.48.0" vite: "npm:^7.3.1" @@ -1937,6 +2368,13 @@ __metadata: languageName: unknown linkType: soft +"axe-core@npm:^4.2.0": + version: 4.11.1 + resolution: "axe-core@npm:4.11.1" + checksum: 10c0/1e6997454b61c7c9a4d740f395952835dcf87f2c04fd81577217d68634d197d602c224f9e8f17b22815db4c117a2519980cfc8911fc0027c54a6d8ebca47c6a7 + languageName: node + linkType: hard + "axios@npm:^1.12.0": version: 1.13.6 resolution: "axios@npm:1.13.6" @@ -2023,6 +2461,15 @@ __metadata: languageName: node linkType: hard +"bundle-name@npm:^4.1.0": + version: 4.1.0 + resolution: "bundle-name@npm:4.1.0" + dependencies: + run-applescript: "npm:^7.0.0" + checksum: 10c0/8e575981e79c2bcf14d8b1c027a3775c095d362d1382312f444a7c861b0e21513c0bd8db5bd2b16e50ba0709fa622d4eab6b53192d222120305e68359daece29 + languageName: node + linkType: hard + "cacache@npm:^20.0.1": version: 20.0.3 resolution: "cacache@npm:20.0.3" @@ -2066,6 +2513,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.2.0": + version: 5.3.3 + resolution: "chai@npm:5.3.3" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10c0/b360fd4d38861622e5010c2f709736988b05c7f31042305fa3f4e9911f6adb80ccfb4e302068bf8ed10e835c2e2520cba0f5edc13d878b886987e5aa62483f53 + languageName: node + linkType: hard + "chai@npm:^6.2.2": version: 6.2.2 resolution: "chai@npm:6.2.2" @@ -2093,6 +2553,13 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.1.1": + version: 2.1.3 + resolution: "check-error@npm:2.1.3" + checksum: 10c0/878e99038fb6476316b74668cd6a498c7e66df3efe48158fa40db80a06ba4258742ac3ee2229c4a2a98c5e73f5dff84eb3e50ceb6b65bbd8f831eafc8338607d + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -2100,6 +2567,25 @@ __metadata: languageName: node linkType: hard +"chromatic@npm:^13.3.4": + version: 13.3.5 + resolution: "chromatic@npm:13.3.5" + peerDependencies: + "@chromatic-com/cypress": ^0.*.* || ^1.0.0 + "@chromatic-com/playwright": ^0.*.* || ^1.0.0 + peerDependenciesMeta: + "@chromatic-com/cypress": + optional: true + "@chromatic-com/playwright": + optional: true + bin: + chroma: dist/bin.js + chromatic: dist/bin.js + chromatic-cli: dist/bin.js + checksum: 10c0/58b3d7984db000f8c7b605788569a24c3f3cd41bb6b2d3a94f18acc9ff11ce6c6881f795c8390a94ff721ccfcf8a2d7942e78a54a1f70294a7b3d35ccc382154 + languageName: node + linkType: hard + "cli-width@npm:^4.1.0": version: 4.1.0 resolution: "cli-width@npm:4.1.0" @@ -2270,6 +2756,13 @@ __metadata: languageName: node linkType: hard +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247 + languageName: node + linkType: hard + "deep-equal@npm:~1.0.1": version: 1.0.1 resolution: "deep-equal@npm:1.0.1" @@ -2284,6 +2777,30 @@ __metadata: languageName: node linkType: hard +"default-browser-id@npm:^5.0.0": + version: 5.0.1 + resolution: "default-browser-id@npm:5.0.1" + checksum: 10c0/5288b3094c740ef3a86df9b999b04ff5ba4dee6b64e7b355c0fff5217752c8c86908d67f32f6cba9bb4f9b7b61a1b640c0a4f9e34c57e0ff3493559a625245ee + languageName: node + linkType: hard + +"default-browser@npm:^5.2.1": + version: 5.5.0 + resolution: "default-browser@npm:5.5.0" + dependencies: + bundle-name: "npm:^4.1.0" + default-browser-id: "npm:^5.0.0" + checksum: 10c0/576593b617b17a7223014b4571bfe1c06a2581a4eb8b130985d90d253afa3f40999caec70eb0e5776e80d4af6a41cce91018cd3f86e57ad578bf59e46fb19abe + languageName: node + linkType: hard + +"define-lazy-prop@npm:^3.0.0": + version: 3.0.0 + resolution: "define-lazy-prop@npm:3.0.0" + checksum: 10c0/5ab0b2bf3fa58b3a443140bbd4cd3db1f91b985cc8a246d330b9ac3fc0b6a325a6d82bddc0b055123d745b3f9931afeea74a5ec545439a1630b9c8512b0eeb49 + languageName: node + linkType: hard + "defu@npm:^6.1.4": version: 6.1.4 resolution: "defu@npm:6.1.4" @@ -2340,6 +2857,15 @@ __metadata: languageName: node linkType: hard +"doctrine@npm:^3.0.0": + version: 3.0.0 + resolution: "doctrine@npm:3.0.0" + dependencies: + esutils: "npm:^2.0.2" + checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520 + languageName: node + linkType: hard + "dom-accessibility-api@npm:^0.5.9": version: 0.5.16 resolution: "dom-accessibility-api@npm:0.5.16" @@ -2386,6 +2912,13 @@ __metadata: languageName: node linkType: hard +"empathic@npm:^2.0.0": + version: 2.0.0 + resolution: "empathic@npm:2.0.0" + checksum: 10c0/7d3b14b04a93b35c47bcc950467ec914fd241cd9acc0269b0ea160f13026ec110f520c90fae64720fde72cc1757b57f3f292fb606617b7fccac1f4d008a76506 + languageName: node + linkType: hard + "encodeurl@npm:^2.0.0": version: 2.0.0 resolution: "encodeurl@npm:2.0.0" @@ -2449,7 +2982,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.27.0": +"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0, esbuild@npm:^0.27.0": version: 0.27.4 resolution: "esbuild@npm:0.27.4" dependencies: @@ -2583,6 +3116,18 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-storybook@npm:^10.3.3": + version: 10.3.3 + resolution: "eslint-plugin-storybook@npm:10.3.3" + dependencies: + "@typescript-eslint/utils": "npm:^8.48.0" + peerDependencies: + eslint: ">=8" + storybook: ^10.3.3 + checksum: 10c0/501a07db230aefa5bb76882fe7b0a3e9a5db87fc29bbcc96b25e880a2ee97a81ff871cf364cb09e9ed9b67bc7d6cd0541755fd0ac778d3b68124289a4fdecde4 + languageName: node + linkType: hard + "eslint-scope@npm:^8.4.0": version: 8.4.0 resolution: "eslint-scope@npm:8.4.0" @@ -2674,6 +3219,16 @@ __metadata: languageName: node linkType: hard +"esprima@npm:~4.0.0": + version: 4.0.1 + resolution: "esprima@npm:4.0.1" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: 10c0/ad4bab9ead0808cf56501750fd9d3fb276f6b105f987707d059005d57e182d18a7c9ec7f3a01794ebddcca676773e42ca48a32d67a250c9d35e009ca613caba3 + languageName: node + linkType: hard + "esquery@npm:^1.5.0": version: 1.7.0 resolution: "esquery@npm:1.7.0" @@ -2787,6 +3342,13 @@ __metadata: languageName: node linkType: hard +"filesize@npm:^10.0.12": + version: 10.1.6 + resolution: "filesize@npm:10.1.6" + checksum: 10c0/9a196d64da4e947b8c0d294be09a3dfa7a634434a1fc5fb3465f1c9acc1237ea0363f245ba6e24477ea612754d942bc964d86e0e500905a72e9e0e17ae1bbdbc + languageName: node + linkType: hard + "find-file-up@npm:^2.0.1": version: 2.0.1 resolution: "find-file-up@npm:2.0.1" @@ -2894,6 +3456,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -2904,6 +3476,15 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" @@ -2981,7 +3562,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^13.0.0": +"glob@npm:^13.0.0, glob@npm:^13.0.1": version: 13.0.6 resolution: "glob@npm:13.0.6" dependencies: @@ -3262,7 +3843,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.1": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -3271,6 +3852,15 @@ __metadata: languageName: node linkType: hard +"is-docker@npm:^3.0.0": + version: 3.0.0 + resolution: "is-docker@npm:3.0.0" + bin: + is-docker: cli.js + checksum: 10c0/d2c4f8e6d3e34df75a5defd44991b6068afad4835bb783b902fa12d13ebdb8f41b2a199dcb0b5ed2cb78bfee9e4c0bbdb69c2d9646f4106464674d3e697a5856 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -3294,6 +3884,17 @@ __metadata: languageName: node linkType: hard +"is-inside-container@npm:^1.0.0": + version: 1.0.0 + resolution: "is-inside-container@npm:1.0.0" + dependencies: + is-docker: "npm:^3.0.0" + bin: + is-inside-container: cli.js + checksum: 10c0/a8efb0e84f6197e6ff5c64c52890fa9acb49b7b74fed4da7c95383965da6f0fa592b4dbd5e38a79f87fc108196937acdbcd758fcefc9b140e479b39ce1fcd1cd + languageName: node + linkType: hard + "is-node-process@npm:^1.2.0": version: 1.2.0 resolution: "is-node-process@npm:1.2.0" @@ -3315,6 +3916,15 @@ __metadata: languageName: node linkType: hard +"is-wsl@npm:^3.1.0": + version: 3.1.1 + resolution: "is-wsl@npm:3.1.1" + dependencies: + is-inside-container: "npm:^1.0.0" + checksum: 10c0/7e5023522bfb8f27de4de960b0d82c4a8146c0bddb186529a3616d78b5bbbfc19ef0c5fc60d0b3a3cc0bf95a415fbdedc18454310ea3049587c879b07ace5107 + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -3455,7 +4065,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.2.3": +"json5@npm:^2.2.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" bin: @@ -3476,7 +4086,7 @@ __metadata: languageName: node linkType: hard -"jsonfile@npm:^6.0.1": +"jsonfile@npm:^6.0.1, jsonfile@npm:^6.1.0": version: 6.2.0 resolution: "jsonfile@npm:6.2.0" dependencies: @@ -3720,6 +4330,13 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.0, loupe@npm:^3.1.4": + version: 3.2.1 + resolution: "loupe@npm:3.2.1" + checksum: 10c0/910c872cba291309664c2d094368d31a68907b6f5913e989d301b5c25f30e97d76d77f23ab3bf3b46d0f601ff0b6af8810c10c31b91d2c6b2f132809ca2cc705 + languageName: node + linkType: hard + "lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": version: 11.2.6 resolution: "lru-cache@npm:11.2.6" @@ -3759,7 +4376,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.21": +"magic-string@npm:^0.30.0, magic-string@npm:^0.30.21": version: 0.30.21 resolution: "magic-string@npm:0.30.21" dependencies: @@ -3885,6 +4502,13 @@ __metadata: languageName: node linkType: hard +"minimist@npm:^1.2.6": + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 + languageName: node + linkType: hard + "minipass-collect@npm:^2.0.1": version: 2.0.1 resolution: "minipass-collect@npm:2.0.1" @@ -3961,6 +4585,13 @@ __metadata: languageName: node linkType: hard +"mrmime@npm:^2.0.0": + version: 2.0.1 + resolution: "mrmime@npm:2.0.1" + checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 + languageName: node + linkType: hard + "ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -4103,6 +4734,18 @@ __metadata: languageName: node linkType: hard +"open@npm:^10.2.0": + version: 10.2.0 + resolution: "open@npm:10.2.0" + dependencies: + default-browser: "npm:^5.2.1" + define-lazy-prop: "npm:^3.0.0" + is-inside-container: "npm:^1.0.0" + wsl-utils: "npm:^0.1.0" + checksum: 10c0/5a36d0c1fd2f74ce553beb427ca8b8494b623fc22c6132d0c1688f246a375e24584ea0b44c67133d9ab774fa69be8e12fbe1ff12504b1142bd960fb09671948f + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -4226,6 +4869,13 @@ __metadata: languageName: node linkType: hard +"pathval@npm:^2.0.0": + version: 2.0.1 + resolution: "pathval@npm:2.0.1" + checksum: 10c0/460f4709479fbf2c45903a65655fc8f0a5f6d808f989173aeef5fdea4ff4f303dc13f7870303999add60ec49d4c14733895c0a869392e9866f1091fa64fd7581 + languageName: node + linkType: hard + "picocolors@npm:1.1.1, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -4240,6 +4890,37 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.58.2": + version: 1.58.2 + resolution: "playwright-core@npm:1.58.2" + bin: + playwright-core: cli.js + checksum: 10c0/5aa15b2b764e6ffe738293a09081a6f7023847a0dbf4cd05fe10eed2e25450d321baf7482f938f2d2eb330291e197fa23e57b29a5b552b89927ceb791266225b + languageName: node + linkType: hard + +"playwright@npm:^1.58.2": + version: 1.58.2 + resolution: "playwright@npm:1.58.2" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.58.2" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10c0/d060d9b7cc124bd8b5dffebaab5e84f6b34654a553758fe7b19cc598dfbee93f6ecfbdc1832b40a6380ae04eade86ef3285ba03aa0b136799e83402246dc0727 + languageName: node + linkType: hard + +"pngjs@npm:^7.0.0": + version: 7.0.0 + resolution: "pngjs@npm:7.0.0" + checksum: 10c0/0d4c7a0fd476a9c33df7d0a2a73e1d56537628a668841f6995c2bca070cf30819f9254a64363266bc14ef2fee47659dd3b4f2b18eec7ab65143015139f497b38 + languageName: node + linkType: hard + "postcss@npm:^8.5.6, postcss@npm:^8.5.8": version: 8.5.8 resolution: "postcss@npm:8.5.8" @@ -4297,7 +4978,34 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^19.2.0": +"react-docgen-typescript@npm:^2.2.2": + version: 2.4.0 + resolution: "react-docgen-typescript@npm:2.4.0" + peerDependencies: + typescript: ">= 4.3.x" + checksum: 10c0/18e3e1c80d28abcdd72e62261d2f70b0904d9b088f9c2ebe485ffee5e46f5735208bc174a20ed2772112b3ca6432b5f3d5f0ac345872fe76e541f84543e49e50 + languageName: node + linkType: hard + +"react-docgen@npm:^8.0.0, react-docgen@npm:^8.0.2": + version: 8.0.3 + resolution: "react-docgen@npm:8.0.3" + dependencies: + "@babel/core": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.0" + "@babel/types": "npm:^7.28.2" + "@types/babel__core": "npm:^7.20.5" + "@types/babel__traverse": "npm:^7.20.7" + "@types/doctrine": "npm:^0.0.9" + "@types/resolve": "npm:^1.20.2" + doctrine: "npm:^3.0.0" + resolve: "npm:^1.22.1" + strip-indent: "npm:^4.0.0" + checksum: 10c0/0231fb9177bc7c633f3d1f228eebb0ee90a2f0feac50b1869ef70b0a3683b400d7875547a2d5168f2619b63d4cc29d7c45ae33d3f621fc67a7fa6790ac2049f6 + languageName: node + linkType: hard + +"react-dom@npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0, react-dom@npm:^19.2.0": version: 19.2.4 resolution: "react-dom@npm:19.2.4" dependencies: @@ -4322,13 +5030,26 @@ __metadata: languageName: node linkType: hard -"react@npm:^19.2.0": +"react@npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0, react@npm:^19.2.0": version: 19.2.4 resolution: "react@npm:19.2.4" checksum: 10c0/cd2c9ff67a720799cc3b38a516009986f7fc4cb8d3e15716c6211cf098d1357ee3e348ab05ad0600042bbb0fd888530ba92e329198c92eafa0994f5213396596 languageName: node linkType: hard +"recast@npm:^0.23.5": + version: 0.23.11 + resolution: "recast@npm:0.23.11" + dependencies: + ast-types: "npm:^0.16.1" + esprima: "npm:~4.0.0" + source-map: "npm:~0.6.1" + tiny-invariant: "npm:^1.3.3" + tslib: "npm:^2.0.1" + checksum: 10c0/45b520a8f0868a5a24ecde495be9de3c48e69a54295d82a7331106554b75cfba75d16c909959d056e9ceed47a1be5e061e2db8b9ecbcd6ba44c2f3ef9a47bd18 + languageName: node + linkType: hard + "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -4383,6 +5104,19 @@ __metadata: languageName: node linkType: hard +"resolve@npm:^1.22.1, resolve@npm:^1.22.8": + version: 1.22.11 + resolution: "resolve@npm:1.22.11" + dependencies: + is-core-module: "npm:^2.16.1" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/f657191507530f2cbecb5815b1ee99b20741ea6ee02a59c57028e9ec4c2c8d7681afcc35febbd554ac0ded459db6f2d8153382c53a2f266cee2575e512674409 + languageName: node + linkType: hard + "resolve@patch:resolve@npm%3A1.22.8#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" @@ -4396,6 +5130,19 @@ __metadata: languageName: node linkType: hard +"resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": + version: 1.22.11 + resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin::version=1.22.11&hash=c3c19d" + dependencies: + is-core-module: "npm:^2.16.1" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/ee5b182f2e37cb1165465e58c6abc797fec0a80b5ba3231607beb4677db0c9291ac010c47cf092b6daa2b7f518d69a0e21888e7e2b633f68d501a874212a8c63 + languageName: node + linkType: hard + "retry@npm:^0.13.1": version: 0.13.1 resolution: "retry@npm:0.13.1" @@ -4565,6 +5312,13 @@ __metadata: languageName: node linkType: hard +"run-applescript@npm:^7.0.0": + version: 7.1.0 + resolution: "run-applescript@npm:7.1.0" + checksum: 10c0/ab826c57c20f244b2ee807704b1ef4ba7f566aa766481ae5922aac785e2570809e297c69afcccc3593095b538a8a77d26f2b2e9a1d9dffee24e0e039502d1a03 + languageName: node + linkType: hard + "safe-buffer@npm:5.2.1": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" @@ -4650,6 +5404,17 @@ __metadata: languageName: node linkType: hard +"sirv@npm:^3.0.2": + version: 3.0.2 + resolution: "sirv@npm:3.0.2" + dependencies: + "@polka/url": "npm:^1.0.0-next.24" + mrmime: "npm:^2.0.0" + totalist: "npm:^3.0.0" + checksum: 10c0/5930e4397afdb14fbae13751c3be983af4bda5c9aadec832607dc2af15a7162f7d518c71b30e83ae3644b9a24cea041543cc969e5fe2b80af6ce8ea3174b2d04 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -4692,6 +5457,13 @@ __metadata: languageName: node linkType: hard +"source-map@npm:~0.6.1": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + "ssri@npm:^13.0.0": version: 13.0.1 resolution: "ssri@npm:13.0.1" @@ -4729,6 +5501,33 @@ __metadata: languageName: node linkType: hard +"storybook@npm:^10.3.3": + version: 10.3.3 + resolution: "storybook@npm:10.3.3" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@storybook/icons": "npm:^2.0.1" + "@testing-library/jest-dom": "npm:^6.9.1" + "@testing-library/user-event": "npm:^14.6.1" + "@vitest/expect": "npm:3.2.4" + "@vitest/spy": "npm:3.2.4" + esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0" + open: "npm:^10.2.0" + recast: "npm:^0.23.5" + semver: "npm:^7.7.3" + use-sync-external-store: "npm:^1.5.0" + ws: "npm:^8.18.0" + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + bin: + storybook: ./dist/bin/dispatcher.js + checksum: 10c0/f61e199dfb11a02be6004a3d72c0ecd062f1770d60d480ecf42a6af8a6c49f9082b17c37fde2eea58ed53de35e7b190c95bcad8c8e4d47f9419d577826e0c00c + languageName: node + linkType: hard + "streamroller@npm:^3.1.5": version: 3.1.5 resolution: "streamroller@npm:3.1.5" @@ -4767,6 +5566,22 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^7.1.0": + version: 7.2.0 + resolution: "strip-ansi@npm:7.2.0" + dependencies: + ansi-regex: "npm:^6.2.2" + checksum: 10c0/544d13b7582f8254811ea97db202f519e189e59d35740c46095897e254e4f1aa9fe1524a83ad6bc5ad67d4dd6c0281d2e0219ed62b880a6238a16a17d375f221 + languageName: node + linkType: hard + +"strip-bom@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-bom@npm:3.0.0" + checksum: 10c0/51201f50e021ef16672593d7434ca239441b7b760e905d9f33df6e4f3954ff54ec0e0a06f100d028af0982d6f25c35cd5cda2ce34eaebccd0250b8befb90d8f1 + languageName: node + linkType: hard + "strip-indent@npm:^3.0.0": version: 3.0.0 resolution: "strip-indent@npm:3.0.0" @@ -4776,6 +5591,13 @@ __metadata: languageName: node linkType: hard +"strip-indent@npm:^4.0.0": + version: 4.1.1 + resolution: "strip-indent@npm:4.1.1" + checksum: 10c0/5b23dd5934be0ef6b6fe1b802887f83e56ad9dcd9f6c3896a637da2c6c3a6da3fdf3e51354a98e6cccb6f1c41863e7b9b9deaa348639dfd35f71f3549edb4dff + languageName: node + linkType: hard + "strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -4826,6 +5648,13 @@ __metadata: languageName: node linkType: hard +"tiny-invariant@npm:^1.3.3": + version: 1.3.3 + resolution: "tiny-invariant@npm:1.3.3" + checksum: 10c0/65af4a07324b591a059b35269cd696aba21bef2107f29b9f5894d83cc143159a204b299553435b03874ebb5b94d019afa8b8eff241c8a4cfee95872c2e1c1c4a + languageName: node + linkType: hard + "tinybench@npm:^2.9.0": version: 2.9.0 resolution: "tinybench@npm:2.9.0" @@ -4850,6 +5679,13 @@ __metadata: languageName: node linkType: hard +"tinyrainbow@npm:^2.0.0": + version: 2.0.0 + resolution: "tinyrainbow@npm:2.0.0" + checksum: 10c0/c83c52bef4e0ae7fb8ec6a722f70b5b6fa8d8be1c85792e829f56c0e1be94ab70b293c032dc5048d4d37cfe678f1f5babb04bdc65fd123098800148ca989184f + languageName: node + linkType: hard + "tinyrainbow@npm:^3.0.3": version: 3.1.0 resolution: "tinyrainbow@npm:3.1.0" @@ -4857,6 +5693,13 @@ __metadata: languageName: node linkType: hard +"tinyspy@npm:^4.0.3": + version: 4.0.4 + resolution: "tinyspy@npm:4.0.4" + checksum: 10c0/a8020fc17799251e06a8398dcc352601d2770aa91c556b9531ecd7a12581161fd1c14e81cbdaff0c1306c93bfdde8ff6d1c1a3f9bbe6d91604f0fd4e01e2f1eb + languageName: node + linkType: hard + "tldts-core@npm:^7.0.26": version: 7.0.26 resolution: "tldts-core@npm:7.0.26" @@ -4882,6 +5725,13 @@ __metadata: languageName: node linkType: hard +"totalist@npm:^3.0.0": + version: 3.0.1 + resolution: "totalist@npm:3.0.1" + checksum: 10c0/4bb1fadb69c3edbef91c73ebef9d25b33bbf69afe1e37ce544d5f7d13854cda15e47132f3e0dc4cafe300ddb8578c77c50a65004d8b6e97e77934a69aa924863 + languageName: node + linkType: hard + "tough-cookie@npm:^6.0.0, tough-cookie@npm:^6.0.1": version: 6.0.1 resolution: "tough-cookie@npm:6.0.1" @@ -4909,7 +5759,25 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.4.0": +"ts-dedent@npm:^2.0.0": + version: 2.2.0 + resolution: "ts-dedent@npm:2.2.0" + checksum: 10c0/175adea838468cc2ff7d5e97f970dcb798bbcb623f29c6088cb21aa2880d207c5784be81ab1741f56b9ac37840cbaba0c0d79f7f8b67ffe61c02634cafa5c303 + languageName: node + linkType: hard + +"tsconfig-paths@npm:^4.2.0": + version: 4.2.0 + resolution: "tsconfig-paths@npm:4.2.0" + dependencies: + json5: "npm:^2.2.2" + minimist: "npm:^1.2.6" + strip-bom: "npm:^3.0.0" + checksum: 10c0/09a5877402d082bb1134930c10249edeebc0211f36150c35e1c542e5b91f1047b1ccf7da1e59babca1ef1f014c525510f4f870de7c9bda470c73bb4e2721b3ea + languageName: node + linkType: hard + +"tslib@npm:^2.0.1, tslib@npm:^2.4.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -5033,6 +5901,18 @@ __metadata: languageName: node linkType: hard +"unplugin@npm:^2.3.5": + version: 2.3.11 + resolution: "unplugin@npm:2.3.11" + dependencies: + "@jridgewell/remapping": "npm:^2.3.5" + acorn: "npm:^8.15.0" + picomatch: "npm:^4.0.3" + webpack-virtual-modules: "npm:^0.6.2" + checksum: 10c0/273c1eab0eca4470c7317428689295c31dbe8ab0b306504de9f03cd20c156debb4131bef24b27ac615862958c5dd950a3951d26c0723ea774652ab3624149cff + languageName: node + linkType: hard + "until-async@npm:^3.0.2": version: 3.0.2 resolution: "until-async@npm:3.0.2" @@ -5063,6 +5943,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.5.0": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b + languageName: node + linkType: hard + "vary@npm:^1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -5261,6 +6150,13 @@ __metadata: languageName: node linkType: hard +"webpack-virtual-modules@npm:^0.6.2": + version: 0.6.2 + resolution: "webpack-virtual-modules@npm:0.6.2" + checksum: 10c0/5ffbddf0e84bf1562ff86cf6fcf039c74edf09d78358a6904a09bbd4484e8bb6812dc385fe14330b715031892dcd8423f7a88278b57c9f5002c84c2860179add + languageName: node + linkType: hard + "whatwg-mimetype@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-mimetype@npm:5.0.0" @@ -5368,6 +6264,30 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.18.0, ws@npm:^8.19.0": + version: 8.20.0 + resolution: "ws@npm:8.20.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/956ac5f11738c914089b65878b9223692ace77337ba55379ae68e1ecbeae9b47a0c6eb9403688f609999a58c80d83d99865fe0029b229d308b08c1ef93d4ea14 + languageName: node + linkType: hard + +"wsl-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "wsl-utils@npm:0.1.0" + dependencies: + is-wsl: "npm:^3.1.0" + checksum: 10c0/44318f3585eb97be994fc21a20ddab2649feaf1fbe893f1f866d936eea3d5f8c743bec6dc02e49fbdd3c0e69e9b36f449d90a0b165a4f47dd089747af4cf2377 + languageName: node + linkType: hard + "xml-name-validator@npm:^5.0.0": version: 5.0.0 resolution: "xml-name-validator@npm:5.0.0" From 683443673eed74d5c82b594d90412275e93a6ee0 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 19:28:55 +0300 Subject: [PATCH 05/12] chore: remove unused code --- src/features/auth/model/stores/authStore/authStore.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/auth/model/stores/authStore/authStore.spec.ts b/src/features/auth/model/stores/authStore/authStore.spec.ts index 19bcdc0..6da7981 100644 --- a/src/features/auth/model/stores/authStore/authStore.spec.ts +++ b/src/features/auth/model/stores/authStore/authStore.spec.ts @@ -11,7 +11,6 @@ const server = setupServer( apiCalls.loginMock, apiCalls.registerMock, apiCalls.logoutMock, - apiCalls.refreshMock, ); const loginSpy = vi.spyOn(apiCalls, "login"); From c41f02f505290e6704cab48f8dab85a12b82e5f4 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 19:29:37 +0300 Subject: [PATCH 06/12] chore: export modules --- src/features/auth/api/calls/index.ts | 2 +- src/features/auth/model/index.ts | 5 ++++- src/features/auth/model/stores/index.ts | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/features/auth/model/stores/index.ts diff --git a/src/features/auth/api/calls/index.ts b/src/features/auth/api/calls/index.ts index 3673624..dd68271 100644 --- a/src/features/auth/api/calls/index.ts +++ b/src/features/auth/api/calls/index.ts @@ -1,4 +1,4 @@ export * from "./login"; export * from "./register"; export * from "./logout"; -export * from "../../../../shared/api/calls/refresh"; +export * from "./mocks"; diff --git a/src/features/auth/model/index.ts b/src/features/auth/model/index.ts index 4e97778..c3082d2 100644 --- a/src/features/auth/model/index.ts +++ b/src/features/auth/model/index.ts @@ -3,4 +3,7 @@ export * from "./types/service"; export * from "./types/store"; // Stores -export * from "./stores/authStore/authStore"; +export * from "./stores"; + +// Selectors +export * from "./selectors"; diff --git a/src/features/auth/model/stores/index.ts b/src/features/auth/model/stores/index.ts new file mode 100644 index 0000000..9df9c1c --- /dev/null +++ b/src/features/auth/model/stores/index.ts @@ -0,0 +1 @@ +export { useAuthStore } from "./authStore/authStore"; From e4630a7fcbba2eba2a71544134798ebbdd603c0b Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 19:30:45 +0300 Subject: [PATCH 07/12] feat(LoginForm): create LoginForm component with basic logic, test coverage and storybook placeholder --- .../auth/ui/LoginForm/LoginForm.spec.tsx | 58 +++++++++++++++++++ .../auth/ui/LoginForm/LoginForm.stories.ts | 12 ++++ src/features/auth/ui/LoginForm/LoginForm.tsx | 41 ++++++++++++- 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/features/auth/ui/LoginForm/LoginForm.spec.tsx create mode 100644 src/features/auth/ui/LoginForm/LoginForm.stories.ts 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 ( -
Login Form
+
+ + + +
); } From c006a94c4db580603843c773a843fc9306186d24 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 19:57:27 +0300 Subject: [PATCH 08/12] feat(auth): add selectStatusIsLoading selector to get information whether auth store status equals "loading" --- src/features/auth/model/selectors/index.ts | 1 + .../selectStatusIsLoading.spec.ts | 18 ++++++++++++++++++ .../selectStatusIsLoading.ts | 4 ++++ 3 files changed, 23 insertions(+) create mode 100644 src/features/auth/model/selectors/selectStatusIsLoading/selectStatusIsLoading.spec.ts create mode 100644 src/features/auth/model/selectors/selectStatusIsLoading/selectStatusIsLoading.ts diff --git a/src/features/auth/model/selectors/index.ts b/src/features/auth/model/selectors/index.ts index f81592d..9358ad5 100644 --- a/src/features/auth/model/selectors/index.ts +++ b/src/features/auth/model/selectors/index.ts @@ -1,2 +1,3 @@ export * from "./selectFormValid/selectFormValid"; export * from "./selectAuthData/selectAuthData"; +export * from "./selectStatusIsLoading/selectStatusIsLoading"; diff --git a/src/features/auth/model/selectors/selectStatusIsLoading/selectStatusIsLoading.spec.ts b/src/features/auth/model/selectors/selectStatusIsLoading/selectStatusIsLoading.spec.ts new file mode 100644 index 0000000..495b1bd --- /dev/null +++ b/src/features/auth/model/selectors/selectStatusIsLoading/selectStatusIsLoading.spec.ts @@ -0,0 +1,18 @@ +import { useAuthStore } from "../../stores"; +import { selectStatusIsLoading } from "./selectStatusIsLoading"; + +describe("selectStatusIsLoading", () => { + afterEach(() => { + useAuthStore.getState().reset(); + }); + + it("should return true when status is 'loading'", () => { + useAuthStore.setState({ status: "loading" }); + expect(selectStatusIsLoading(useAuthStore.getState())).toBe(true); + }); + + it("should return false when status is not 'loading'", () => { + useAuthStore.setState({ status: "idle" }); + expect(selectStatusIsLoading(useAuthStore.getState())).toBe(false); + }); +}); diff --git a/src/features/auth/model/selectors/selectStatusIsLoading/selectStatusIsLoading.ts b/src/features/auth/model/selectors/selectStatusIsLoading/selectStatusIsLoading.ts new file mode 100644 index 0000000..c30f6a1 --- /dev/null +++ b/src/features/auth/model/selectors/selectStatusIsLoading/selectStatusIsLoading.ts @@ -0,0 +1,4 @@ +import type { AuthStore } from "../../types/store"; + +export const selectStatusIsLoading = (state: AuthStore) => + state.status === "loading"; From 70bcf7fdcfd9c99fe379b7caf44ccebd498a5cc0 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 20:03:34 +0300 Subject: [PATCH 09/12] feat(LoginForm): add isLoading check to LoginForm, add new test cases --- src/features/auth/ui/LoginForm/LoginForm.spec.tsx | 14 +++++++++++++- src/features/auth/ui/LoginForm/LoginForm.tsx | 11 +++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/features/auth/ui/LoginForm/LoginForm.spec.tsx b/src/features/auth/ui/LoginForm/LoginForm.spec.tsx index 447259d..df67a4f 100644 --- a/src/features/auth/ui/LoginForm/LoginForm.spec.tsx +++ b/src/features/auth/ui/LoginForm/LoginForm.spec.tsx @@ -21,9 +21,20 @@ describe("LoginForm", () => { expect(screen.getByRole("button", { name: /login/i })).toBeDisabled(); }); - it("enables submit button when form is valid", () => { + it("disables submit button when auth store status equals loading", () => { useAuthStore.getState().setEmail(MOCK_EMAIL); useAuthStore.getState().setPassword(MOCK_PASSWORD); + useAuthStore.setState({ status: "loading" }); + + render(); + expect(screen.getByRole("button", { name: /login/i })).toBeDisabled(); + }); + + it("enables submit button when form is valid and auth store status isn't equal to loading", () => { + useAuthStore.getState().setEmail(MOCK_EMAIL); + useAuthStore.getState().setPassword(MOCK_PASSWORD); + useAuthStore.setState({ status: "idle" }); + render(); expect(screen.getByRole("button", { name: /login/i })).toBeEnabled(); }); @@ -49,6 +60,7 @@ describe("LoginForm", () => { useAuthStore.getState().setEmail(MOCK_EMAIL); useAuthStore.getState().setPassword(MOCK_PASSWORD); + useAuthStore.setState({ status: "idle" }); render(); const submitButton = screen.getByRole("button", { name: /login/i }); diff --git a/src/features/auth/ui/LoginForm/LoginForm.tsx b/src/features/auth/ui/LoginForm/LoginForm.tsx index 530f02e..5b1c8f7 100644 --- a/src/features/auth/ui/LoginForm/LoginForm.tsx +++ b/src/features/auth/ui/LoginForm/LoginForm.tsx @@ -1,4 +1,8 @@ -import { selectFormValid, useAuthStore } from "../../model"; +import { + selectFormValid, + selectStatusIsLoading, + useAuthStore, +} from "../../model"; import type { SubmitEvent, ChangeEvent } from "react"; /** @@ -8,6 +12,9 @@ export function LoginForm() { const { formData, setEmail, setPassword, login } = useAuthStore(); const formValid = useAuthStore(selectFormValid); + const isLoading = useAuthStore(selectStatusIsLoading); + + const disabled = !formValid || isLoading; const handleEmailChange = (e: ChangeEvent) => { setEmail(e.target.value); @@ -36,7 +43,7 @@ export function LoginForm() { value={formData?.password?.value ?? ""} onChange={handlePasswordChange} /> - From 4d854d08a3cb0492d234ed87ff2b56f8319595ac Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 20:29:16 +0300 Subject: [PATCH 10/12] test(LoginForm): replace setState with userEvent.type for authenticity --- .../auth/ui/LoginForm/LoginForm.spec.tsx | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/src/features/auth/ui/LoginForm/LoginForm.spec.tsx b/src/features/auth/ui/LoginForm/LoginForm.spec.tsx index df67a4f..5bbe66e 100644 --- a/src/features/auth/ui/LoginForm/LoginForm.spec.tsx +++ b/src/features/auth/ui/LoginForm/LoginForm.spec.tsx @@ -10,61 +10,80 @@ describe("LoginForm", () => { vi.restoreAllMocks(); }); - it("should render", () => { + it("should render form", () => { render(); + const form = screen.getByRole("form"); - expect(screen.getByRole("form")).toBeInTheDocument(); + expect(form).toBeInTheDocument(); }); it("disables submit button when form is invalid", () => { render(); - expect(screen.getByRole("button", { name: /login/i })).toBeDisabled(); + const loginButton = screen.getByRole("button", { name: /login/i }); + + expect(loginButton).toBeDisabled(); }); - it("disables submit button when auth store status equals loading", () => { - useAuthStore.getState().setEmail(MOCK_EMAIL); - useAuthStore.getState().setPassword(MOCK_PASSWORD); + it("disables submit button when auth store status equals loading", async () => { useAuthStore.setState({ status: "loading" }); render(); - expect(screen.getByRole("button", { name: /login/i })).toBeDisabled(); + + const emailInput = screen.getByRole("textbox", { name: /email/i }); + const passwordInput = screen.getByLabelText(/password/i); + const loginButton = screen.getByRole("button", { name: /login/i }); + await userEvent.type(emailInput, MOCK_EMAIL); + await userEvent.type(passwordInput, MOCK_PASSWORD); + + expect(loginButton).toBeDisabled(); }); - it("enables submit button when form is valid and auth store status isn't equal to loading", () => { - useAuthStore.getState().setEmail(MOCK_EMAIL); - useAuthStore.getState().setPassword(MOCK_PASSWORD); + it("enables submit button when form is valid and auth store status isn't equal to loading", async () => { useAuthStore.setState({ status: "idle" }); render(); - expect(screen.getByRole("button", { name: /login/i })).toBeEnabled(); + + const emailInput = screen.getByRole("textbox", { name: /email/i }); + const passwordInput = screen.getByLabelText(/password/i); + const loginButton = screen.getByRole("button", { name: /login/i }); + await userEvent.type(emailInput, MOCK_EMAIL); + await userEvent.type(passwordInput, MOCK_PASSWORD); + + expect(loginButton).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); + + const storeEmailValue = selectAuthData(useAuthStore.getState()).email; + expect(storeEmailValue).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, - ); + + const storePasswordValue = selectAuthData(useAuthStore.getState()).password; + expect(storePasswordValue).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); useAuthStore.setState({ status: "idle" }); render(); + + const emailInput = screen.getByRole("textbox", { name: /email/i }); + const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /login/i }); + + await userEvent.type(emailInput, MOCK_EMAIL); + await userEvent.type(passwordInput, MOCK_PASSWORD); await userEvent.click(submitButton); + expect(loginSpy).toHaveBeenCalled(); }); }); From 576665f32b0f3fb82a69cf29fa8b64f32f6adb9b Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 20:42:37 +0300 Subject: [PATCH 11/12] test(LoginForm): clarify test case names --- src/features/auth/ui/LoginForm/LoginForm.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/auth/ui/LoginForm/LoginForm.spec.tsx b/src/features/auth/ui/LoginForm/LoginForm.spec.tsx index 5bbe66e..8cbfa93 100644 --- a/src/features/auth/ui/LoginForm/LoginForm.spec.tsx +++ b/src/features/auth/ui/LoginForm/LoginForm.spec.tsx @@ -52,7 +52,7 @@ describe("LoginForm", () => { expect(loginButton).toBeEnabled(); }); - it("updates email when user types", async () => { + it("updates email value in auth store when user types", async () => { render(); const emailInput = screen.getByRole("textbox", { name: /email/i }); await userEvent.type(emailInput, MOCK_EMAIL); @@ -61,7 +61,7 @@ describe("LoginForm", () => { expect(storeEmailValue).toBe(MOCK_EMAIL); }); - it("updates password when user types", async () => { + it("updates password value in auth store when user types", async () => { render(); const passwordInput = screen.getByLabelText(/password/i); await userEvent.type(passwordInput, MOCK_PASSWORD); From 83fbf83bae6e67789d6e3d3d4b5862f2bccaa98c Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 24 Mar 2026 20:57:35 +0300 Subject: [PATCH 12/12] feat(RegisterForm): add basic RegisterFormComponent with test coverage and storybook placeholder --- .../ui/RegisterForm/RegisterForm.spec.tsx | 115 ++++++++++++++++++ .../ui/RegisterForm/RegisterForm.stories.ts | 12 ++ .../auth/ui/RegisterForm/RegisterForm.tsx | 61 +++++++++- 3 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/features/auth/ui/RegisterForm/RegisterForm.spec.tsx create mode 100644 src/features/auth/ui/RegisterForm/RegisterForm.stories.ts diff --git a/src/features/auth/ui/RegisterForm/RegisterForm.spec.tsx b/src/features/auth/ui/RegisterForm/RegisterForm.spec.tsx new file mode 100644 index 0000000..367301f --- /dev/null +++ b/src/features/auth/ui/RegisterForm/RegisterForm.spec.tsx @@ -0,0 +1,115 @@ +import { render, screen } from "@testing-library/react"; +import { RegisterForm } from "./RegisterForm"; +import { selectAuthData, useAuthStore } from "../../model"; +import { MOCK_NEW_EMAIL, MOCK_PASSWORD } from "../../api"; +import userEvent from "@testing-library/user-event"; + +describe("RegisterForm", () => { + afterEach(() => { + useAuthStore.getState().reset(); + vi.restoreAllMocks(); + }); + + it("should render form", () => { + render(); + + const form = screen.getByRole("form"); + expect(form).toBeInTheDocument(); + }); + + it("should disable button when form is invalid", async () => { + render(); + + const registerButton = screen.getByRole("button", { name: /register/i }); + + expect(registerButton).toBeDisabled(); + }); + + it("should disable button when auth store status equals loading", async () => { + useAuthStore.setState({ status: "loading" }); + + render(); + + const emailInput = screen.getByRole("textbox", { name: /email/i }); + const passwordInput = screen.getByLabelText(/password/i); + const confirmPasswordInput = screen.getByLabelText(/confirm/i); + const registerButton = screen.getByRole("button", { name: /register/i }); + + await userEvent.type(emailInput, MOCK_NEW_EMAIL); + await userEvent.type(passwordInput, MOCK_PASSWORD); + await userEvent.type(confirmPasswordInput, MOCK_PASSWORD); + + expect(registerButton).toBeDisabled(); + }); + + it("should disable button when password and confirm password do not match", async () => { + render(); + + const emailInput = screen.getByRole("textbox", { name: /email/i }); + const passwordInput = screen.getByLabelText(/password/i); + const confirmPasswordInput = screen.getByLabelText(/confirm/i); + const registerButton = screen.getByRole("button", { name: /register/i }); + + await userEvent.type(emailInput, MOCK_NEW_EMAIL); + await userEvent.type(passwordInput, MOCK_PASSWORD); + await userEvent.type(confirmPasswordInput, MOCK_PASSWORD + "1"); + + expect(registerButton).toBeDisabled(); + }); + + it("should enable button when password and confirm password match and auth store status isn't equal to loading", async () => { + useAuthStore.setState({ status: "idle" }); + + render(); + + const emailInput = screen.getByRole("textbox", { name: /email/i }); + const passwordInput = screen.getByLabelText(/password/i); + const confirmPasswordInput = screen.getByLabelText(/confirm/i); + const registerButton = screen.getByRole("button", { name: /register/i }); + + await userEvent.type(emailInput, MOCK_NEW_EMAIL); + await userEvent.type(passwordInput, MOCK_PASSWORD); + await userEvent.type(confirmPasswordInput, MOCK_PASSWORD); + + expect(registerButton).not.toBeDisabled(); + }); + + it("should change email value in auth store when user types", async () => { + render(); + const emailInput = screen.getByRole("textbox", { name: /email/i }); + await userEvent.type(emailInput, MOCK_NEW_EMAIL); + + const storeEmailValue = selectAuthData(useAuthStore.getState()).email; + + expect(storeEmailValue).toBe(MOCK_NEW_EMAIL); + }); + + it("should change password value in auth store when user types", async () => { + render(); + const passwordInput = screen.getByLabelText(/password/i); + await userEvent.type(passwordInput, MOCK_PASSWORD); + + const storePasswordValue = selectAuthData(useAuthStore.getState()).password; + + expect(storePasswordValue).toBe(MOCK_PASSWORD); + }); + + it("should perform register api call when user clicks register button", async () => { + const registerSpy = vi.spyOn(useAuthStore.getState(), "register"); + useAuthStore.setState({ status: "idle" }); + + render(); + + const emailInput = screen.getByRole("textbox", { name: /email/i }); + const passwordInput = screen.getByLabelText(/password/i); + const confirmPasswordInput = screen.getByLabelText(/confirm/i); + const registerButton = screen.getByRole("button", { name: /register/i }); + + await userEvent.type(emailInput, MOCK_NEW_EMAIL); + await userEvent.type(passwordInput, MOCK_PASSWORD); + await userEvent.type(confirmPasswordInput, MOCK_PASSWORD); + await userEvent.click(registerButton); + + expect(registerSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/features/auth/ui/RegisterForm/RegisterForm.stories.ts b/src/features/auth/ui/RegisterForm/RegisterForm.stories.ts new file mode 100644 index 0000000..d9e36b6 --- /dev/null +++ b/src/features/auth/ui/RegisterForm/RegisterForm.stories.ts @@ -0,0 +1,12 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { RegisterForm } from "./RegisterForm"; + +const meta: Meta = { + component: RegisterForm, + title: "features/auth/RegisterForm", +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/features/auth/ui/RegisterForm/RegisterForm.tsx b/src/features/auth/ui/RegisterForm/RegisterForm.tsx index 65b0a22..f12da9f 100644 --- a/src/features/auth/ui/RegisterForm/RegisterForm.tsx +++ b/src/features/auth/ui/RegisterForm/RegisterForm.tsx @@ -1,5 +1,64 @@ +import { useState, type ChangeEvent, type SubmitEvent } from "react"; +import { + selectFormValid, + selectStatusIsLoading, + useAuthStore, +} from "../../model"; + +/** + * Register form component + */ export function RegisterForm() { + const { formData, setEmail, setPassword, register } = useAuthStore(); + + const [confirmedPassword, setConfirmedPassword] = useState(""); + + const formValid = useAuthStore(selectFormValid); + const isLoading = useAuthStore(selectStatusIsLoading); + + const passwordMatch = formData?.password?.value === confirmedPassword; + const disabled = isLoading || !formValid || !passwordMatch; + + const handleEmailChange = (e: ChangeEvent) => { + setEmail(e.target.value); + }; + + const handlePasswordChange = (e: ChangeEvent) => { + setPassword(e.target.value); + }; + + const handleConfirmChange = (e: ChangeEvent) => { + setConfirmedPassword(e.target.value); + }; + + const handleSubmit = (e: SubmitEvent) => { + e.preventDefault(); + register(); + }; + return ( -
Register Form
+
+ + + + +
); }