Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions frontend/src/features/application/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/http'
import {
fetchApplications,
fetchApplicationDetail,
submitReview,
claimApplication,
releaseApplication,
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 sends list query params', 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 requests the task by id', 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 posts form values to the 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('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 })

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 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')

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 })
})

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 })
})
})
36 changes: 36 additions & 0 deletions frontend/src/features/consignment/hooks/useConsignmentList.test.ts
Original file line number Diff line number Diff line change
@@ -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('loads consignments on mount', 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)
})
})
33 changes: 33 additions & 0 deletions frontend/src/features/consignment/service.test.ts
Original file line number Diff line number Diff line change
@@ -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 search and pagination params', 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)
})
})
30 changes: 30 additions & 0 deletions frontend/src/hooks/useDebounce.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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('keeps the previous value until the delay elapses', () => {
const { result, rerender } = renderHook(({ val }) => useDebounce(val, 400), {
initialProps: { val: 'hello' },
})

expect(result.current).toBe('hello')

rerender({ val: 'world' })
expect(result.current).toBe('hello')

act(() => {
vi.advanceTimersByTime(400)
})

expect(result.current).toBe('world')
})
})
75 changes: 75 additions & 0 deletions frontend/src/http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, it, expect, vi, beforeEach, afterEach } 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(),
},
}))

function jsonOk(body: unknown) {
return {
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'application/json' }),
json: (): Promise<unknown> => Promise.resolve(body),
}
}

describe('http client', () => {
const mockedFetch = vi.fn()

beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('fetch', mockedFetch)
})

afterEach(() => {
vi.unstubAllGlobals()
})

it('attaches authorization header when attachToken is true', async () => {
vi.mocked(userManager).getUser.mockResolvedValue({
access_token: 'test-bearer-token',
} as never)
mockedFetch.mockResolvedValue(jsonOk({ status: 'ok' }))

const res = await http.request({
url: 'http://localhost:8080/api/v1/test',
attachToken: true,
})

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('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.mock.calls[0]?.[0]).toBe('http://localhost:8080/api/v1/search?q=tea&page=1')
})

it('throws when the response is not ok', async () => {
mockedFetch.mockResolvedValue({ ok: false, status: 404 })

await expect(
http.request({
url: 'http://localhost:8080/api/v1/notfound',
}),
).rejects.toThrow('HTTP error! status: 404')
})
})
2 changes: 1 addition & 1 deletion frontend/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/utils/date.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 missing', () => {
expect(formatDateForTable()).toBe('-')
expect(formatDateForTable('')).toBe('-')
})

it('formats a valid ISO date for table display', () => {
expect(formatDateForTable('2026-08-10T10:00:00Z')).toBe('Aug 10, 2026')
})
})
Loading