From 910e6987a3886959cc0a0701d7de07e5fc70e037 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 10 Aug 2026 15:29:29 +0530 Subject: [PATCH 1/3] chore(unit test): expand unit test coverage across HTTP layer, service,s custom hooks and utilites --- .../src/features/application/service.test.ts | 101 ++++++++++++++++++ .../hooks/useConsignmentList.test.ts | 36 +++++++ .../src/features/consignment/service.test.ts | 33 ++++++ .../user/hooks/useSignOutHandler.test.ts | 22 ++++ frontend/src/hooks/useDebounce.test.ts | 36 +++++++ frontend/src/http.test.ts | 81 ++++++++++++++ frontend/src/http.ts | 2 +- frontend/src/utils/date.test.ts | 15 +++ 8 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 frontend/src/features/application/service.test.ts create mode 100644 frontend/src/features/consignment/hooks/useConsignmentList.test.ts create mode 100644 frontend/src/features/consignment/service.test.ts create mode 100644 frontend/src/features/user/hooks/useSignOutHandler.test.ts create mode 100644 frontend/src/hooks/useDebounce.test.ts create mode 100644 frontend/src/http.test.ts create mode 100644 frontend/src/utils/date.test.ts diff --git a/frontend/src/features/application/service.test.ts b/frontend/src/features/application/service.test.ts new file mode 100644 index 00000000..8174d215 --- /dev/null +++ b/frontend/src/features/application/service.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/http' +import { fetchApplications, fetchApplicationDetail, submitReview, submitFeedback, getDownloadUrl } from './service' + +vi.mock('@/http', () => ({ + API_BASE_URL: 'http://localhost:8080', + http: { + request: vi.fn(), + }, +})) + +describe('application service', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('fetchApplications queries API with formatted parameters', async () => { + const mockResponse = { data: { items: [], total: 0, page: 1, pageSize: 20 } } + vi.mocked(http.request).mockResolvedValue(mockResponse) + + const result = await fetchApplications({ consignmentId: 'C123', page: 1, pageSize: 20 }) + + expect(http.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8080/api/v1/applications', + method: 'GET', + params: { consignmentId: 'C123', page: 1, pageSize: 20 }, + attachToken: true, + }), + ) + expect(result).toEqual(mockResponse.data) + }) + + it('fetchApplicationDetail queries specific task id endpoint', async () => { + const mockApp = { taskId: 'T-100', title: 'Inspection Application' } + vi.mocked(http.request).mockResolvedValue({ data: mockApp }) + + const result = await fetchApplicationDetail('T-100') + + expect(http.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8080/api/v1/applications/T-100', + method: 'GET', + attachToken: true, + }), + ) + expect(result).toEqual(mockApp) + }) + + it('submitReview sends POST request to review endpoint', async () => { + const mockResult = { status: 'APPROVED' } + vi.mocked(http.request).mockResolvedValue({ data: mockResult }) + + const formValues = { decision: 'APPROVED', remarks: 'Looks good' } + const result = await submitReview('T-100', formValues) + + expect(http.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8080/api/v1/applications/T-100/review', + method: 'POST', + data: formValues, + attachToken: true, + }), + ) + expect(result).toEqual(mockResult) + }) + + it('submitFeedback sends POST request to feedback endpoint', async () => { + const mockResult = { status: 'FEEDBACK_REQUESTED' } + vi.mocked(http.request).mockResolvedValue({ data: mockResult }) + + const content = { comment: 'Please clarify declaration' } + const result = await submitFeedback('T-100', content) + + expect(http.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8080/api/v1/applications/T-100/feedback', + method: 'POST', + data: content, + attachToken: true, + }), + ) + expect(result).toEqual(mockResult) + }) + + it('getDownloadUrl fetches download URL metadata', async () => { + const mockMetadata = { download_url: 'http://localhost:8080/downloads/file.pdf', expires_at: 1700000000 } + vi.mocked(http.request).mockResolvedValue({ data: mockMetadata }) + + const result = await getDownloadUrl('file-key-123') + + expect(http.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8080/api/v1/storage/file-key-123', + method: 'GET', + attachToken: true, + }), + ) + expect(result).toEqual({ url: 'http://localhost:8080/downloads/file.pdf', expiresAt: 1700000000 }) + }) +}) diff --git a/frontend/src/features/consignment/hooks/useConsignmentList.test.ts b/frontend/src/features/consignment/hooks/useConsignmentList.test.ts new file mode 100644 index 00000000..56b29c90 --- /dev/null +++ b/frontend/src/features/consignment/hooks/useConsignmentList.test.ts @@ -0,0 +1,36 @@ +import { renderHook, waitFor } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { useConsignmentList } from './useConsignmentList' +import * as consignmentService from '../service' + +vi.mock('../service', () => ({ + fetchConsignments: vi.fn(), +})) + +describe('useConsignmentList', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('fetches consignments on mount and updates state', async () => { + const mockItems = [{ id: 'C1', consignmentNumber: 'CN-100', status: 'SUBMITTED' }] + vi.mocked(consignmentService.fetchConsignments).mockResolvedValue({ + items: mockItems as never, + total: 1, + page: 1, + pageSize: 20, + }) + + const { result } = renderHook(() => useConsignmentList('')) + + expect(result.current.status.loading).toBe(true) + + await waitFor(() => { + expect(result.current.status.loading).toBe(false) + }) + + expect(result.current.data).toEqual(mockItems) + expect(result.current.pagination.total).toBe(1) + expect(result.current.pagination.totalPages).toBe(1) + }) +}) diff --git a/frontend/src/features/consignment/service.test.ts b/frontend/src/features/consignment/service.test.ts new file mode 100644 index 00000000..11c12816 --- /dev/null +++ b/frontend/src/features/consignment/service.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from '@/http' +import { fetchConsignments } from './service' + +vi.mock('@/http', () => ({ + API_BASE_URL: 'http://localhost:8080', + http: { + request: vi.fn(), + }, +})) + +describe('consignment service', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('fetchConsignments sends GET request with pagination and search query parameters', async () => { + const mockResponse = { data: { items: [], total: 0, page: 1, pageSize: 20 } } + vi.mocked(http.request).mockResolvedValue(mockResponse) + + const result = await fetchConsignments({ q: 'tea', page: 1, pageSize: 20 }) + + expect(http.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8080/api/v1/consignments', + method: 'GET', + params: { q: 'tea', page: 1, pageSize: 20 }, + attachToken: true, + }), + ) + expect(result).toEqual(mockResponse.data) + }) +}) diff --git a/frontend/src/features/user/hooks/useSignOutHandler.test.ts b/frontend/src/features/user/hooks/useSignOutHandler.test.ts new file mode 100644 index 00000000..255cbe28 --- /dev/null +++ b/frontend/src/features/user/hooks/useSignOutHandler.test.ts @@ -0,0 +1,22 @@ +import { renderHook } from '@testing-library/react' +import { describe, it, expect, vi } from 'vitest' +import { useSignOutHandler } from './useSignOutHandler' +import { useAuth } from 'react-oidc-context' + +vi.mock('react-oidc-context', () => ({ + useAuth: vi.fn(), +})) + +describe('useSignOutHandler', () => { + it('calls signoutRedirect when returned handler is executed', () => { + const signoutRedirectMock = vi.fn().mockResolvedValue(undefined) + vi.mocked(useAuth).mockReturnValue({ + signoutRedirect: signoutRedirectMock, + } as never) + + const { result } = renderHook(() => useSignOutHandler()) + result.current() + + expect(signoutRedirectMock).toHaveBeenCalledTimes(1) + }) +}) diff --git a/frontend/src/hooks/useDebounce.test.ts b/frontend/src/hooks/useDebounce.test.ts new file mode 100644 index 00000000..ad821d94 --- /dev/null +++ b/frontend/src/hooks/useDebounce.test.ts @@ -0,0 +1,36 @@ +import { renderHook, act } from '@testing-library/react' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { useDebounce } from './useDebounce' + +describe('useDebounce', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('returns initial value immediately', () => { + const { result } = renderHook(() => useDebounce('hello', 400)) + expect(result.current).toBe('hello') + }) + + it('updates debounced value after specified delay', () => { + const { result, rerender } = renderHook(({ val }) => useDebounce(val, 400), { + initialProps: { val: 'hello' }, + }) + + expect(result.current).toBe('hello') + + rerender({ val: 'world' }) + // Before delay, still initial value + expect(result.current).toBe('hello') + + act(() => { + vi.advanceTimersByTime(400) + }) + + expect(result.current).toBe('world') + }) +}) diff --git a/frontend/src/http.test.ts b/frontend/src/http.test.ts new file mode 100644 index 00000000..25cfa998 --- /dev/null +++ b/frontend/src/http.test.ts @@ -0,0 +1,81 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { http } from './http' +import { userManager } from '@/features/user/oidcUserManager' + +vi.mock('./runtimeConfig', () => ({ + getRequiredEnv: () => 'http://localhost:8080', +})) + +vi.mock('@/features/user/oidcUserManager', () => ({ + userManager: { + getUser: vi.fn(), + }, +})) + +describe('http client', () => { + const mockedFetch = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('fetch', mockedFetch) + }) + + it('attaches authorization header when attachToken is true', async () => { + vi.spyOn(userManager, 'getUser').mockResolvedValue({ + access_token: 'test-bearer-token', + } as never) + + const mockResponse = { + ok: true, + headers: new Headers({ 'content-type': 'application/json' }), + json: (): Promise => Promise.resolve({ status: 'ok' }), + } + mockedFetch.mockResolvedValue(mockResponse) + + const res = await http.request({ + url: 'http://localhost:8080/api/v1/test', + attachToken: true, + }) + + expect(mockedFetch).toHaveBeenCalledWith( + 'http://localhost:8080/api/v1/test', + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer test-bearer-token', + }), + }), + ) + expect(res).toEqual({ data: { status: 'ok' } }) + }) + + it('formats query string parameters correctly', async () => { + const mockResponse = { + ok: true, + headers: new Headers({ 'content-type': 'application/json' }), + json: (): Promise => Promise.resolve({ items: [] }), + } + mockedFetch.mockResolvedValue(mockResponse) + + await http.request({ + url: 'http://localhost:8080/api/v1/search', + params: { q: 'tea', page: 1, empty: undefined, nullVal: null }, + }) + + expect(mockedFetch).toHaveBeenCalledWith('http://localhost:8080/api/v1/search?q=tea&page=1', expect.anything()) + }) + + it('throws error when response is not ok', async () => { + const mockResponse = { + ok: false, + status: 404, + } + mockedFetch.mockResolvedValue(mockResponse) + + await expect( + http.request({ + url: 'http://localhost:8080/api/v1/notfound', + }), + ).rejects.toThrow('HTTP error! status: 404') + }) +}) diff --git a/frontend/src/http.ts b/frontend/src/http.ts index 15c1fb15..2e5b2438 100644 --- a/frontend/src/http.ts +++ b/frontend/src/http.ts @@ -14,7 +14,7 @@ interface RequestConfig { } export const http = { - request: async (config: RequestConfig) => { + request: async (config: RequestConfig): Promise<{ data: unknown }> => { let url = config.url if (config.params) { const searchParams = new URLSearchParams() diff --git a/frontend/src/utils/date.test.ts b/frontend/src/utils/date.test.ts new file mode 100644 index 00000000..6038c587 --- /dev/null +++ b/frontend/src/utils/date.test.ts @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest' +import { formatDateForTable } from './date' + +describe('formatDateForTable', () => { + it('returns "-" when date string is undefined or empty', () => { + expect(formatDateForTable()).toBe('-') + expect(formatDateForTable('')).toBe('-') + }) + + it('formats valid ISO date string correctly', () => { + const formatted = formatDateForTable('2026-08-10T10:00:00Z') + expect(formatted).not.toBe('-') + expect(formatted).toContain('2026') + }) +}) From 9f84034adeca4fc59d69557192d936bcfbbdc8b1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 11 Aug 2026 11:11:14 +0530 Subject: [PATCH 2/3] test(frontend): assert complete date in deterministic test environment and ensure prettier compliance --- frontend/src/utils/date.test.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/date.test.ts b/frontend/src/utils/date.test.ts index 6038c587..96d4d07b 100644 --- a/frontend/src/utils/date.test.ts +++ b/frontend/src/utils/date.test.ts @@ -1,7 +1,21 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { formatDateForTable } from './date' +vi.mock('@/i18n', () => ({ + default: { + resolvedLanguage: 'en-US', + }, +})) + describe('formatDateForTable', () => { + beforeEach(() => { + vi.stubEnv('TZ', 'UTC') + }) + + afterEach(() => { + vi.unstubAllEnvs() + }) + it('returns "-" when date string is undefined or empty', () => { expect(formatDateForTable()).toBe('-') expect(formatDateForTable('')).toBe('-') @@ -9,7 +23,9 @@ describe('formatDateForTable', () => { it('formats valid ISO date string correctly', () => { const formatted = formatDateForTable('2026-08-10T10:00:00Z') - expect(formatted).not.toBe('-') + expect(formatted).toBe('Aug 10, 2026') + expect(formatted).toContain('Aug') + expect(formatted).toContain('10') expect(formatted).toContain('2026') }) }) From 4031da4fd440190b3309f513bc02deaec8ee642f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 5 Sep 2026 10:11:42 +0530 Subject: [PATCH 3/3] test(frontend): keep unit tests focused on necessary behavior Drop the sign-out pass-through case and redundant debounce/date assertions, and tighten the HTTP client tests around auth, query params, and error handling. Co-authored-by: Cursor --- .../src/features/application/service.test.ts | 63 ++++++++++++++++--- .../hooks/useConsignmentList.test.ts | 2 +- .../src/features/consignment/service.test.ts | 2 +- .../user/hooks/useSignOutHandler.test.ts | 22 ------- frontend/src/hooks/useDebounce.test.ts | 8 +-- frontend/src/http.test.ts | 58 ++++++++--------- frontend/src/utils/date.test.ts | 10 +-- 7 files changed, 87 insertions(+), 78 deletions(-) delete mode 100644 frontend/src/features/user/hooks/useSignOutHandler.test.ts diff --git a/frontend/src/features/application/service.test.ts b/frontend/src/features/application/service.test.ts index 8174d215..3cbdfb50 100644 --- a/frontend/src/features/application/service.test.ts +++ b/frontend/src/features/application/service.test.ts @@ -1,6 +1,14 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { http } from '@/http' -import { fetchApplications, fetchApplicationDetail, submitReview, submitFeedback, getDownloadUrl } from './service' +import { + fetchApplications, + fetchApplicationDetail, + submitReview, + claimApplication, + releaseApplication, + submitFeedback, + getDownloadUrl, +} from './service' vi.mock('@/http', () => ({ API_BASE_URL: 'http://localhost:8080', @@ -14,7 +22,7 @@ describe('application service', () => { vi.clearAllMocks() }) - it('fetchApplications queries API with formatted parameters', async () => { + it('fetchApplications sends list query params', async () => { const mockResponse = { data: { items: [], total: 0, page: 1, pageSize: 20 } } vi.mocked(http.request).mockResolvedValue(mockResponse) @@ -31,7 +39,7 @@ describe('application service', () => { expect(result).toEqual(mockResponse.data) }) - it('fetchApplicationDetail queries specific task id endpoint', async () => { + it('fetchApplicationDetail requests the task by id', async () => { const mockApp = { taskId: 'T-100', title: 'Inspection Application' } vi.mocked(http.request).mockResolvedValue({ data: mockApp }) @@ -47,7 +55,7 @@ describe('application service', () => { expect(result).toEqual(mockApp) }) - it('submitReview sends POST request to review endpoint', async () => { + it('submitReview posts form values to the review endpoint', async () => { const mockResult = { status: 'APPROVED' } vi.mocked(http.request).mockResolvedValue({ data: mockResult }) @@ -65,7 +73,35 @@ describe('application service', () => { expect(result).toEqual(mockResult) }) - it('submitFeedback sends POST request to feedback endpoint', async () => { + it('claimApplication posts to the claim endpoint', async () => { + vi.mocked(http.request).mockResolvedValue({ data: undefined }) + + await claimApplication('T-100') + + expect(http.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8080/api/v1/applications/T-100/claim', + method: 'POST', + attachToken: true, + }), + ) + }) + + it('releaseApplication posts to the release endpoint', async () => { + vi.mocked(http.request).mockResolvedValue({ data: undefined }) + + await releaseApplication('T-100') + + expect(http.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8080/api/v1/applications/T-100/release', + method: 'POST', + attachToken: true, + }), + ) + }) + + it('submitFeedback posts content to the feedback endpoint', async () => { const mockResult = { status: 'FEEDBACK_REQUESTED' } vi.mocked(http.request).mockResolvedValue({ data: mockResult }) @@ -83,9 +119,10 @@ describe('application service', () => { expect(result).toEqual(mockResult) }) - it('getDownloadUrl fetches download URL metadata', async () => { - const mockMetadata = { download_url: 'http://localhost:8080/downloads/file.pdf', expires_at: 1700000000 } - vi.mocked(http.request).mockResolvedValue({ data: mockMetadata }) + it('getDownloadUrl maps storage metadata to url and expiry', async () => { + vi.mocked(http.request).mockResolvedValue({ + data: { download_url: 'http://localhost:8080/downloads/file.pdf', expires_at: 1700000000 }, + }) const result = await getDownloadUrl('file-key-123') @@ -98,4 +135,14 @@ describe('application service', () => { ) expect(result).toEqual({ url: 'http://localhost:8080/downloads/file.pdf', expiresAt: 1700000000 }) }) + + it('getDownloadUrl resolves a relative download path against the API base', async () => { + vi.mocked(http.request).mockResolvedValue({ + data: { download_url: '/downloads/file.pdf', expires_at: 1700000000 }, + }) + + const result = await getDownloadUrl('file-key-123') + + expect(result).toEqual({ url: 'http://localhost:8080/downloads/file.pdf', expiresAt: 1700000000 }) + }) }) diff --git a/frontend/src/features/consignment/hooks/useConsignmentList.test.ts b/frontend/src/features/consignment/hooks/useConsignmentList.test.ts index 56b29c90..75b9fcc5 100644 --- a/frontend/src/features/consignment/hooks/useConsignmentList.test.ts +++ b/frontend/src/features/consignment/hooks/useConsignmentList.test.ts @@ -12,7 +12,7 @@ describe('useConsignmentList', () => { vi.clearAllMocks() }) - it('fetches consignments on mount and updates state', async () => { + it('loads consignments on mount', async () => { const mockItems = [{ id: 'C1', consignmentNumber: 'CN-100', status: 'SUBMITTED' }] vi.mocked(consignmentService.fetchConsignments).mockResolvedValue({ items: mockItems as never, diff --git a/frontend/src/features/consignment/service.test.ts b/frontend/src/features/consignment/service.test.ts index 11c12816..2bbaf9ca 100644 --- a/frontend/src/features/consignment/service.test.ts +++ b/frontend/src/features/consignment/service.test.ts @@ -14,7 +14,7 @@ describe('consignment service', () => { vi.clearAllMocks() }) - it('fetchConsignments sends GET request with pagination and search query parameters', async () => { + it('fetchConsignments sends search and pagination params', async () => { const mockResponse = { data: { items: [], total: 0, page: 1, pageSize: 20 } } vi.mocked(http.request).mockResolvedValue(mockResponse) diff --git a/frontend/src/features/user/hooks/useSignOutHandler.test.ts b/frontend/src/features/user/hooks/useSignOutHandler.test.ts deleted file mode 100644 index 255cbe28..00000000 --- a/frontend/src/features/user/hooks/useSignOutHandler.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { renderHook } from '@testing-library/react' -import { describe, it, expect, vi } from 'vitest' -import { useSignOutHandler } from './useSignOutHandler' -import { useAuth } from 'react-oidc-context' - -vi.mock('react-oidc-context', () => ({ - useAuth: vi.fn(), -})) - -describe('useSignOutHandler', () => { - it('calls signoutRedirect when returned handler is executed', () => { - const signoutRedirectMock = vi.fn().mockResolvedValue(undefined) - vi.mocked(useAuth).mockReturnValue({ - signoutRedirect: signoutRedirectMock, - } as never) - - const { result } = renderHook(() => useSignOutHandler()) - result.current() - - expect(signoutRedirectMock).toHaveBeenCalledTimes(1) - }) -}) diff --git a/frontend/src/hooks/useDebounce.test.ts b/frontend/src/hooks/useDebounce.test.ts index ad821d94..a6e3d289 100644 --- a/frontend/src/hooks/useDebounce.test.ts +++ b/frontend/src/hooks/useDebounce.test.ts @@ -11,12 +11,7 @@ describe('useDebounce', () => { vi.useRealTimers() }) - it('returns initial value immediately', () => { - const { result } = renderHook(() => useDebounce('hello', 400)) - expect(result.current).toBe('hello') - }) - - it('updates debounced value after specified delay', () => { + it('keeps the previous value until the delay elapses', () => { const { result, rerender } = renderHook(({ val }) => useDebounce(val, 400), { initialProps: { val: 'hello' }, }) @@ -24,7 +19,6 @@ describe('useDebounce', () => { expect(result.current).toBe('hello') rerender({ val: 'world' }) - // Before delay, still initial value expect(result.current).toBe('hello') act(() => { diff --git a/frontend/src/http.test.ts b/frontend/src/http.test.ts index 25cfa998..77015f36 100644 --- a/frontend/src/http.test.ts +++ b/frontend/src/http.test.ts @@ -1,5 +1,4 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { http } from './http' import { userManager } from '@/features/user/oidcUserManager' @@ -13,6 +12,15 @@ vi.mock('@/features/user/oidcUserManager', () => ({ }, })) +function jsonOk(body: unknown) { + return { + ok: true, + status: 200, + headers: new Headers({ 'content-type': 'application/json' }), + json: (): Promise => Promise.resolve(body), + } +} + describe('http client', () => { const mockedFetch = vi.fn() @@ -21,56 +29,42 @@ describe('http client', () => { vi.stubGlobal('fetch', mockedFetch) }) + afterEach(() => { + vi.unstubAllGlobals() + }) + it('attaches authorization header when attachToken is true', async () => { - vi.spyOn(userManager, 'getUser').mockResolvedValue({ + vi.mocked(userManager).getUser.mockResolvedValue({ access_token: 'test-bearer-token', } as never) - - const mockResponse = { - ok: true, - headers: new Headers({ 'content-type': 'application/json' }), - json: (): Promise => Promise.resolve({ status: 'ok' }), - } - mockedFetch.mockResolvedValue(mockResponse) + mockedFetch.mockResolvedValue(jsonOk({ status: 'ok' })) const res = await http.request({ url: 'http://localhost:8080/api/v1/test', attachToken: true, }) - expect(mockedFetch).toHaveBeenCalledWith( - 'http://localhost:8080/api/v1/test', - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer test-bearer-token', - }), - }), - ) + expect(mockedFetch).toHaveBeenCalledTimes(1) + expect(mockedFetch.mock.calls[0]?.[0]).toBe('http://localhost:8080/api/v1/test') + expect(mockedFetch.mock.calls[0]?.[1]).toMatchObject({ + headers: { Authorization: 'Bearer test-bearer-token' }, + }) expect(res).toEqual({ data: { status: 'ok' } }) }) - it('formats query string parameters correctly', async () => { - const mockResponse = { - ok: true, - headers: new Headers({ 'content-type': 'application/json' }), - json: (): Promise => Promise.resolve({ items: [] }), - } - mockedFetch.mockResolvedValue(mockResponse) + it('omits null and undefined query parameters', async () => { + mockedFetch.mockResolvedValue(jsonOk({ items: [] })) await http.request({ url: 'http://localhost:8080/api/v1/search', params: { q: 'tea', page: 1, empty: undefined, nullVal: null }, }) - expect(mockedFetch).toHaveBeenCalledWith('http://localhost:8080/api/v1/search?q=tea&page=1', expect.anything()) + expect(mockedFetch.mock.calls[0]?.[0]).toBe('http://localhost:8080/api/v1/search?q=tea&page=1') }) - it('throws error when response is not ok', async () => { - const mockResponse = { - ok: false, - status: 404, - } - mockedFetch.mockResolvedValue(mockResponse) + it('throws when the response is not ok', async () => { + mockedFetch.mockResolvedValue({ ok: false, status: 404 }) await expect( http.request({ diff --git a/frontend/src/utils/date.test.ts b/frontend/src/utils/date.test.ts index 96d4d07b..bab54683 100644 --- a/frontend/src/utils/date.test.ts +++ b/frontend/src/utils/date.test.ts @@ -16,16 +16,12 @@ describe('formatDateForTable', () => { vi.unstubAllEnvs() }) - it('returns "-" when date string is undefined or empty', () => { + it('returns "-" when date string is missing', () => { expect(formatDateForTable()).toBe('-') expect(formatDateForTable('')).toBe('-') }) - it('formats valid ISO date string correctly', () => { - const formatted = formatDateForTable('2026-08-10T10:00:00Z') - expect(formatted).toBe('Aug 10, 2026') - expect(formatted).toContain('Aug') - expect(formatted).toContain('10') - expect(formatted).toContain('2026') + it('formats a valid ISO date for table display', () => { + expect(formatDateForTable('2026-08-10T10:00:00Z')).toBe('Aug 10, 2026') }) })