From 14c0d1eb09a75392adea7b4fdb8db3dd315c1c80 Mon Sep 17 00:00:00 2001 From: Mykyta Olym Date: Sat, 27 Sep 2025 21:05:05 +0200 Subject: [PATCH 1/3] test(global-modal): add comprehensive unit tests for GlobalModal (Vitest + RTL) --- .idea/.gitignore | 10 ++ .../GlobalModal/GlobalModal.test.tsx | 134 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 src/components/GlobalModal/GlobalModal.test.tsx diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..7bc07ec --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Environment-dependent path to Maven home directory +/mavenHomeManager.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/src/components/GlobalModal/GlobalModal.test.tsx b/src/components/GlobalModal/GlobalModal.test.tsx new file mode 100644 index 0000000..28897d9 --- /dev/null +++ b/src/components/GlobalModal/GlobalModal.test.tsx @@ -0,0 +1,134 @@ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import * as GlobalModalCtx from '@/context/GlobalModalContext'; + +import GlobalModal from './GlobalModal'; + +const { modalState } = vi.hoisted(() => ({ + modalState: { promptLogin: false as boolean, promptError: null as string | null }, +})); + +vi.mock('@/context/GlobalModalContext', () => { + const useGlobalModal = vi.fn(() => modalState); + return { useGlobalModal }; +}); + + +vi.mock('./Modals/LoginModal', () => ({ + __esModule: true, + default: () =>
LOGIN_MODAL
, +})); +vi.mock('./Modals/ErrorModal', () => ({ + __esModule: true, + default: ({ errorMessageKey }: { errorMessageKey: string }) => ( +
{errorMessageKey}
+ ), +})); + +const ensurePortalContainer = () => { + let container = document.getElementById('modal-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'modal-container'; + document.body.appendChild(container); + } + return container; +}; + +beforeEach(() => { + // reset DOM + state + document.body.innerHTML = ''; + modalState.promptLogin = false; + modalState.promptError = null; + + // reset context mock calls + (GlobalModalCtx.useGlobalModal as any).mockClear?.(); +}); + +describe('GlobalModal', () => { + it('renders nothing when neither login nor error is requested', () => { + const container = ensurePortalContainer(); + + render(); + + expect(GlobalModalCtx.useGlobalModal).toHaveBeenCalled(); + + // No modals in portal container nor body + expect(container.querySelector('[data-testid="mock-login-modal"]')).toBeNull(); + expect(container.querySelector('[data-testid="mock-error-modal"]')).toBeNull(); + expect(screen.queryByTestId('mock-login-modal')).toBeNull(); + expect(screen.queryByTestId('mock-error-modal')).toBeNull(); + }); + + it('renders LoginModal when promptLogin is true (into #modal-container)', () => { + const container = ensurePortalContainer(); + modalState.promptLogin = true; + + render(); + + expect(GlobalModalCtx.useGlobalModal).toHaveBeenCalled(); + + // login modal should be inside the portal container + expect(container.querySelector('[data-testid="mock-login-modal"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="mock-error-modal"]')).toBeNull(); + }); + + it('renders ErrorModal with message when promptError is set', () => { + const container = ensurePortalContainer(); + modalState.promptError = 'some.error.key'; + + render(); + + expect(GlobalModalCtx.useGlobalModal).toHaveBeenCalled(); + + const err = container.querySelector('[data-testid="mock-error-modal"]'); + expect(err).not.toBeNull(); + expect(err!.textContent).toBe('some.error.key'); + expect(container.querySelector('[data-testid="mock-login-modal"]')).toBeNull(); + }); + + it('prefers ErrorModal over LoginModal when both flags are set', () => { + const container = ensurePortalContainer(); + modalState.promptLogin = true; + modalState.promptError = 'override.error'; + + render(); + + expect(GlobalModalCtx.useGlobalModal).toHaveBeenCalled(); + + expect(container.querySelector('[data-testid="mock-error-modal"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="mock-login-modal"]')).toBeNull(); + }); + + it('falls back to document.body when #modal-container is missing', () => { + // no ensurePortalContainer() → should portal to body + modalState.promptLogin = true; + + render(); + + expect(GlobalModalCtx.useGlobalModal).toHaveBeenCalled(); + + // login modal should exist in body (since there’s no container) + const login = document.body.querySelector('[data-testid="mock-login-modal"]'); + expect(login).not.toBeNull(); + }); + + it('rerenders correctly when state changes (login → error)', () => { + const container = ensurePortalContainer(); + modalState.promptLogin = true; + + const { rerender } = render(); + expect(container.querySelector('[data-testid="mock-login-modal"]')).not.toBeNull(); + expect(container.querySelector('[data-testid="mock-error-modal"]')).toBeNull(); + + modalState.promptLogin = false; + modalState.promptError = 'err.key'; + rerender(); + + expect(container.querySelector('[data-testid="mock-login-modal"]')).toBeNull(); + const err = container.querySelector('[data-testid="mock-error-modal"]'); + expect(err).not.toBeNull(); + expect(err!.textContent).toBe('err.key'); + }); +}); \ No newline at end of file From 7242401eeb580b78a93ca9af9e56478b408f956f Mon Sep 17 00:00:00 2001 From: Mykyta Olym Date: Sat, 27 Sep 2025 21:10:42 +0200 Subject: [PATCH 2/3] chore: ignore IDE files; remove .idea from tracking --- .gitignore | 1 + .idea/.gitignore | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 .idea/.gitignore diff --git a/.gitignore b/.gitignore index ca901b2..5c610f0 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* todo.md +.idea/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 7bc07ec..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Environment-dependent path to Maven home directory -/mavenHomeManager.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml From 259c147a36cdd5e3b783168a3da129efe51350f8 Mon Sep 17 00:00:00 2001 From: Mykyta Olym Date: Sat, 27 Sep 2025 21:13:57 +0200 Subject: [PATCH 3/3] chore: revert .gitignore change (avoid IDE-specific ignores in repo) --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5c610f0..ca901b2 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,3 @@ npm-debug.log* yarn-debug.log* yarn-error.log* todo.md -.idea/