mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Merge pull request #991 from nowcommunity/codex/create-implementation-plan-for-time-units-error
Handle time unit fetch errors
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals'
|
||||
import request from 'supertest'
|
||||
|
||||
import app from '../../app'
|
||||
import * as timeUnitService from '../../services/timeUnit'
|
||||
import { pool } from '../../utils/db'
|
||||
import { login, resetDatabase, resetDatabaseTimeout, send } from '../utils'
|
||||
|
||||
describe('GET /time-unit endpoints', () => {
|
||||
beforeAll(async () => {
|
||||
await resetDatabase()
|
||||
await login()
|
||||
}, resetDatabaseTimeout)
|
||||
|
||||
afterAll(async () => {
|
||||
await pool.end()
|
||||
})
|
||||
|
||||
it('returns all time units with expected shape', async () => {
|
||||
const { body, status } = await send<Record<string, unknown>[]>('time-unit/all', 'GET')
|
||||
|
||||
expect(status).toBe(200)
|
||||
expect(Array.isArray(body)).toBe(true)
|
||||
expect(body.length).toBeGreaterThan(0)
|
||||
|
||||
const [first] = body
|
||||
expect(first).toHaveProperty('tu_name')
|
||||
expect(first).toHaveProperty('tu_display_name')
|
||||
expect(first).toHaveProperty('rank')
|
||||
expect(first).toHaveProperty('seq_name')
|
||||
expect(first).toHaveProperty('low_bound')
|
||||
expect(first).toHaveProperty('up_bound')
|
||||
})
|
||||
|
||||
it('returns structured 404 response for missing time unit', async () => {
|
||||
const { body, status } = await send<{ message: string }>('time-unit/non-existent-id', 'GET')
|
||||
|
||||
expect(status).toBe(404)
|
||||
expect(body).toEqual({ message: 'Time unit not found' })
|
||||
})
|
||||
|
||||
it('handles unexpected failures gracefully', async () => {
|
||||
const spy = jest.spyOn(timeUnitService, 'getAllTimeUnits').mockRejectedValueOnce(new Error('db unavailable'))
|
||||
|
||||
const response = await request(app).get('/time-unit/all')
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(response.body).toEqual({ message: 'Failed to fetch time units' })
|
||||
|
||||
spy.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -22,17 +22,25 @@ import { writeTimeBound } from '../services/write/timeBound'
|
||||
const router = Router()
|
||||
|
||||
router.get('/all', async (_req, res) => {
|
||||
const time_units = await getAllTimeUnits()
|
||||
return res.status(200).send(time_units)
|
||||
try {
|
||||
const time_units = await getAllTimeUnits()
|
||||
return res.status(200).send(time_units)
|
||||
} catch (_error) {
|
||||
return res.status(500).send({ message: 'Failed to fetch time units' })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/sequences', getSequences)
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
const id = req.params.id
|
||||
const timeUnit = await getTimeUnitDetails(id)
|
||||
if (!timeUnit) return res.status(404).send()
|
||||
return res.status(200).send(timeUnit)
|
||||
try {
|
||||
const timeUnit = await getTimeUnitDetails(id)
|
||||
if (!timeUnit) return res.status(404).send({ message: 'Time unit not found' })
|
||||
return res.status(200).send(timeUnit)
|
||||
} catch (_error) {
|
||||
return res.status(500).send({ message: 'Failed to fetch time unit' })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/localities/:id', async (req, res) => {
|
||||
|
||||
@@ -33,9 +33,9 @@ export const getAllTimeUnits = async () => {
|
||||
tu_name: item.tu_name,
|
||||
tu_display_name: item.tu_display_name,
|
||||
rank: item.rank,
|
||||
low_bound: item.now_tu_bound_now_time_unit_low_bndTonow_tu_bound.age,
|
||||
up_bound: item.now_tu_bound_now_time_unit_up_bndTonow_tu_bound.age,
|
||||
seq_name: item.now_tu_sequence.seq_name,
|
||||
low_bound: item.now_tu_bound_now_time_unit_low_bndTonow_tu_bound?.age ?? null,
|
||||
up_bound: item.now_tu_bound_now_time_unit_up_bndTonow_tu_bound?.age ?? null,
|
||||
seq_name: item.now_tu_sequence?.seq_name ?? null,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
import { TimeUnit, TimeBoundDetailsType } from '@/shared/types'
|
||||
import { useGetTimeBoundTimeUnitsQuery } from '@/redux/timeBoundReducer'
|
||||
import { CircularProgress } from '@mui/material'
|
||||
import { Alert, Button, CircularProgress, Stack } from '@mui/material'
|
||||
import { skipToken } from '@reduxjs/toolkit/query'
|
||||
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
||||
import { MRT_ColumnDef } from 'material-react-table'
|
||||
import { SimpleTable } from '@/components/DetailView/common/SimpleTable'
|
||||
|
||||
export const TimeUnitTab = () => {
|
||||
const { data } = useDetailContext<TimeBoundDetailsType>()
|
||||
const { data: timeUnitsData, isError } = useGetTimeBoundTimeUnitsQuery(data.bid)
|
||||
const { data, mode } = useDetailContext<TimeBoundDetailsType>()
|
||||
const boundId = typeof data?.bid === 'number' ? data.bid : null
|
||||
const queryArg = !mode.new && boundId !== null ? boundId : skipToken
|
||||
|
||||
if (isError) return 'Error loading Time Units.'
|
||||
if (!timeUnitsData) return <CircularProgress />
|
||||
const { data: timeUnitsData, isError, isLoading, isFetching, refetch } = useGetTimeBoundTimeUnitsQuery(queryArg)
|
||||
|
||||
if (queryArg === skipToken)
|
||||
return (
|
||||
<Alert data-testid="time-units-disabled" severity="info">
|
||||
Save this time bound to view related time units.
|
||||
</Alert>
|
||||
)
|
||||
|
||||
if (isError)
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Alert data-testid="time-units-error" severity="error">
|
||||
Could not load Time Units. Please try again.
|
||||
</Alert>
|
||||
<Button data-testid="time-units-retry" onClick={() => void refetch()} variant="outlined">
|
||||
Retry
|
||||
</Button>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
if (isLoading || isFetching || !timeUnitsData) return <CircularProgress />
|
||||
|
||||
const columns: MRT_ColumnDef<TimeUnit>[] = [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { beforeEach, describe, expect, it, jest } from '@jest/globals'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import '@testing-library/jest-dom'
|
||||
import { skipToken } from '@reduxjs/toolkit/query'
|
||||
|
||||
import { TimeUnitTab } from '@/components/TimeBound/Tabs/TimeUnitTab'
|
||||
import { DetailContext, type DetailContextType, modeOptionToMode } from '@/components/DetailView/Context/DetailContext'
|
||||
import type { EditDataType, TimeBoundDetailsType, TimeUnit } from '@/shared/types'
|
||||
|
||||
jest.mock('lodash-es', () => ({
|
||||
cloneDeep: (value: unknown) => value,
|
||||
}))
|
||||
|
||||
jest.mock('@/util/config', () => ({
|
||||
BACKEND_URL: '',
|
||||
ENABLE_WRITE: true,
|
||||
ENV: 'test',
|
||||
}))
|
||||
|
||||
jest.mock('@/components/DetailView/common/SimpleTable', () => ({
|
||||
SimpleTable: ({ data }: { data: TimeUnit[] }) => (
|
||||
<div data-testid="simple-table">
|
||||
{data.map(unit => (
|
||||
<div key={unit.tu_name}>{unit.tu_display_name}</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const mockGetTimeBoundTimeUnitsQuery = jest.fn()
|
||||
|
||||
jest.mock('@/redux/timeBoundReducer', () => ({
|
||||
useGetTimeBoundTimeUnitsQuery: (...args: unknown[]) => mockGetTimeBoundTimeUnitsQuery(...args),
|
||||
}))
|
||||
|
||||
const baseTimeBound: TimeBoundDetailsType = {
|
||||
bid: 1,
|
||||
b_name: 'Test Bound',
|
||||
age: 12,
|
||||
b_comment: '',
|
||||
now_bau: [],
|
||||
} as unknown as TimeBoundDetailsType
|
||||
|
||||
const createDetailContextValue = (overrides: Partial<DetailContextType<TimeBoundDetailsType>> = {}) =>
|
||||
({
|
||||
data: baseTimeBound,
|
||||
mode: modeOptionToMode.read,
|
||||
setMode: () => {},
|
||||
editData: { bid: 1 } as unknown as EditDataType<TimeBoundDetailsType>,
|
||||
setEditData: () => {},
|
||||
textField: () => <></>,
|
||||
bigTextField: () => <></>,
|
||||
dropdown: () => <></>,
|
||||
dropdownWithSearch: () => <></>,
|
||||
radioSelection: () => <></>,
|
||||
validator: () => ({ name: '', error: null }),
|
||||
fieldsWithErrors: {},
|
||||
setFieldsWithErrors: () => {},
|
||||
...overrides,
|
||||
}) as DetailContextType<TimeBoundDetailsType>
|
||||
|
||||
const renderTimeUnitTab = (overrides: Partial<DetailContextType<TimeBoundDetailsType>> = {}) => {
|
||||
const contextValue = createDetailContextValue(overrides)
|
||||
render(
|
||||
<DetailContext.Provider value={contextValue as unknown as DetailContextType<unknown>}>
|
||||
<TimeUnitTab />
|
||||
</DetailContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockGetTimeBoundTimeUnitsQuery.mockReturnValue({
|
||||
data: [],
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
refetch: jest.fn(),
|
||||
})
|
||||
})
|
||||
|
||||
describe('Time Units tab', () => {
|
||||
it('loads time units when bound id is present', async () => {
|
||||
const timeUnits: TimeUnit[] = [
|
||||
{
|
||||
tu_name: 'TU1',
|
||||
tu_display_name: 'Sample Unit',
|
||||
seq_name: 'Sequence 1',
|
||||
rank: 'Stage',
|
||||
low_bound: 1,
|
||||
up_bound: 1,
|
||||
},
|
||||
]
|
||||
|
||||
mockGetTimeBoundTimeUnitsQuery.mockReturnValue({
|
||||
data: timeUnits,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
refetch: jest.fn(),
|
||||
})
|
||||
|
||||
renderTimeUnitTab()
|
||||
|
||||
expect(mockGetTimeBoundTimeUnitsQuery).toHaveBeenCalledWith(1)
|
||||
const unitRow = await screen.findByText('Sample Unit')
|
||||
expect(unitRow).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows retry controls when loading time units fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
const refetch = jest.fn()
|
||||
mockGetTimeBoundTimeUnitsQuery.mockReturnValue({
|
||||
data: undefined,
|
||||
isError: true,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
refetch,
|
||||
})
|
||||
|
||||
renderTimeUnitTab()
|
||||
|
||||
const errorAlert = await screen.findByTestId('time-units-error')
|
||||
expect(errorAlert).toBeTruthy()
|
||||
|
||||
await user.click(screen.getByTestId('time-units-retry'))
|
||||
expect(refetch).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('displays guidance instead of fetching on new time bounds', async () => {
|
||||
renderTimeUnitTab({
|
||||
mode: modeOptionToMode.new,
|
||||
data: { bid: undefined } as unknown as TimeBoundDetailsType,
|
||||
})
|
||||
|
||||
const guidance = await screen.findByTestId('time-units-disabled')
|
||||
expect(guidance).toBeTruthy()
|
||||
expect(mockGetTimeBoundTimeUnitsQuery).toHaveBeenCalledWith(skipToken)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user