add frontend

This commit is contained in:
voltsrage
2026-06-19 17:42:09 +08:00
parent 49973271e5
commit 5f69ce1a95
53 changed files with 6591 additions and 0 deletions
@@ -0,0 +1,37 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import AlertCard from '@/components/alerts/AlertCard.vue'
const openAlert = {
id: 'alert-1',
alertType: 'SepsisWarning',
severity: 'Critical',
status: 'Open',
details: 'SIRS criteria met',
triggeredAt: '2026-06-19T12:00:00Z',
}
const resolvedAlert = {
...openAlert,
id: 'alert-2',
status: 'Resolved',
}
describe('AlertCard', () => {
it('showsAlertTypeAndSeverity', () => {
const wrapper = mount(AlertCard, { props: { alert: openAlert } })
expect(wrapper.text()).toContain('Critical')
expect(wrapper.text()).toContain('SEPSIS_WARNING')
})
it('acknowledgeButtonEmitsEvent', async () => {
const wrapper = mount(AlertCard, { props: { alert: openAlert } })
await wrapper.get('button').trigger('click')
expect(wrapper.emitted('acknowledge')).toHaveLength(1)
})
it('resolvedAlertHidesActions', () => {
const wrapper = mount(AlertCard, { props: { alert: resolvedAlert } })
expect(wrapper.findAll('button')).toHaveLength(0)
})
})
@@ -0,0 +1,32 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Badge from '@/components/ui/Badge.vue'
describe('Badge', () => {
it('rendersCriticalVariant', () => {
const wrapper = mount(Badge, {
props: { variant: 'critical' },
slots: { default: 'High' },
})
const classes = wrapper.classes().join(' ')
expect(classes).toContain('bg-severity-critical/10')
expect(classes).toContain('text-severity-critical')
})
it('rendersWarningVariant', () => {
const wrapper = mount(Badge, {
props: { variant: 'warning' },
slots: { default: 'Med' },
})
const classes = wrapper.classes().join(' ')
expect(classes).toContain('bg-severity-warning/10')
expect(classes).toContain('text-severity-warning')
})
it('rendersSlotContent', () => {
const wrapper = mount(Badge, {
slots: { default: 'NEWS2 8' },
})
expect(wrapper.text()).toBe('NEWS2 8')
})
})
@@ -0,0 +1,70 @@
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import WardTable from '@/components/ward/WardTable.vue'
const { mockPush } = vi.hoisted(() => ({
mockPush: vi.fn(),
}))
vi.mock('vue-router', () => ({
useRouter: () => ({ push: mockPush }),
}))
const patients = [
{
encounterId: 'enc-1',
firstName: 'Alice',
lastName: 'A',
mrn: 'M1',
room: '101',
news2Score: 3,
qsofaScore: 0,
sepsisActive: false,
openAlertCount: 0,
},
{
encounterId: 'enc-2',
firstName: 'Bob',
lastName: 'B',
mrn: 'M2',
room: '102',
news2Score: 5,
qsofaScore: 1,
sepsisActive: false,
openAlertCount: 1,
},
{
encounterId: 'enc-3',
firstName: 'Carol',
lastName: 'C',
mrn: 'M3',
room: '103',
news2Score: 8,
qsofaScore: 2,
sepsisActive: true,
openAlertCount: 3,
},
]
describe('WardTable', () => {
it('rendersAllPatientRows', () => {
const wrapper = mount(WardTable, { props: { patients } })
expect(wrapper.findAll('tbody tr')).toHaveLength(3)
})
it('emitsClickWithEncounterId', async () => {
mockPush.mockClear()
const wrapper = mount(WardTable, { props: { patients } })
await wrapper.findAll('tbody tr')[2].trigger('click')
expect(mockPush).toHaveBeenCalledWith({
name: 'PatientDetail',
params: { encounterId: 'enc-3' },
})
})
it('showsCriticalBadgeForHighNews2', () => {
const wrapper = mount(WardTable, { props: { patients } })
const highRiskRow = wrapper.findAll('tbody tr')[2]
expect(highRiskRow.html()).toContain('text-severity-critical')
})
})
@@ -0,0 +1,64 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { defineComponent, h } from 'vue'
import { mount, flushPromises } from '@vue/test-utils'
import { usePolling } from '@/composables/usePolling'
function mountPolling(fetchFn, intervalMs = 1_000) {
let exposed
const Comp = defineComponent({
setup() {
exposed = usePolling(fetchFn, intervalMs)
return exposed
},
render: () => h('div'),
})
const wrapper = mount(Comp)
return { wrapper, exposed }
}
describe('usePolling', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('callsFetchOnMount', async () => {
const fetchFn = vi.fn().mockResolvedValue({ ok: true })
mountPolling(fetchFn)
await flushPromises()
expect(fetchFn).toHaveBeenCalledTimes(1)
})
it('callsFetchAtInterval', async () => {
const fetchFn = vi.fn().mockResolvedValue(null)
mountPolling(fetchFn, 1_000)
await flushPromises()
expect(fetchFn).toHaveBeenCalledTimes(1)
vi.advanceTimersByTime(1_000)
await flushPromises()
expect(fetchFn).toHaveBeenCalledTimes(2)
})
it('clearsIntervalOnUnmount', async () => {
const fetchFn = vi.fn().mockResolvedValue(null)
const { wrapper } = mountPolling(fetchFn, 1_000)
await flushPromises()
expect(fetchFn).toHaveBeenCalledTimes(1)
wrapper.unmount()
vi.advanceTimersByTime(5_000)
await flushPromises()
expect(fetchFn).toHaveBeenCalledTimes(1)
})
it('setsErrorOnFailure', async () => {
const fetchFn = vi.fn().mockRejectedValue(new Error('network error'))
const { exposed } = mountPolling(fetchFn)
await flushPromises()
expect(exposed.error.value).toBe('network error')
})
})