feature: Doctor Feedback Mode
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import AlertCard from '@/components/alerts/AlertCard.vue'
|
||||
|
||||
const openAlert = {
|
||||
@@ -18,6 +19,10 @@ const resolvedAlert = {
|
||||
}
|
||||
|
||||
describe('AlertCard', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('showsAlertTypeAndSeverity', () => {
|
||||
const wrapper = mount(AlertCard, { props: { alert: openAlert } })
|
||||
expect(wrapper.text()).toContain('Critical')
|
||||
@@ -26,12 +31,15 @@ describe('AlertCard', () => {
|
||||
|
||||
it('acknowledgeButtonEmitsEvent', async () => {
|
||||
const wrapper = mount(AlertCard, { props: { alert: openAlert } })
|
||||
await wrapper.get('button').trigger('click')
|
||||
const ackButton = wrapper.findAll('button').find(b => b.text() === 'Acknowledge')
|
||||
await ackButton.trigger('click')
|
||||
expect(wrapper.emitted('acknowledge')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('resolvedAlertHidesActions', () => {
|
||||
const wrapper = mount(AlertCard, { props: { alert: resolvedAlert } })
|
||||
expect(wrapper.findAll('button')).toHaveLength(0)
|
||||
const actionButtons = wrapper.findAll('button').filter(b => ['Acknowledge', 'Resolve'].includes(b.text()))
|
||||
expect(actionButtons).toHaveLength(0)
|
||||
expect(wrapper.text()).toContain('Useful')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import AlertReasoning from '@/components/alerts/AlertReasoning.vue'
|
||||
|
||||
const sepsisAlert = {
|
||||
@@ -19,6 +20,10 @@ const qsofaAlert = {
|
||||
}
|
||||
|
||||
describe('AlertReasoning', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('showsExplanationForSepsisWarning', () => {
|
||||
const wrapper = mount(AlertReasoning, { props: { alert: sepsisAlert } })
|
||||
expect(wrapper.text()).toContain('SIRS / Sepsis Alert')
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import FeedbackButtons from '@/components/feedback/FeedbackButtons.vue'
|
||||
|
||||
const defaultProps = {
|
||||
alertId: 'alert-1',
|
||||
alertType: 'SepsisWarning',
|
||||
severity: 'Critical',
|
||||
}
|
||||
|
||||
function ratingButtons(wrapper) {
|
||||
return wrapper.findAll('[role="radio"]')
|
||||
}
|
||||
|
||||
function ratingButton(wrapper, label) {
|
||||
return ratingButtons(wrapper).find(b => b.text() === label)
|
||||
}
|
||||
|
||||
describe('FeedbackButtons', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('rendersAllSixRatingButtons', () => {
|
||||
const wrapper = mount(FeedbackButtons, { props: defaultProps })
|
||||
expect(ratingButtons(wrapper)).toHaveLength(6)
|
||||
})
|
||||
|
||||
it('selectingRatingHighlightsButton', async () => {
|
||||
const wrapper = mount(FeedbackButtons, { props: defaultProps })
|
||||
const usefulBtn = ratingButton(wrapper, 'Useful')
|
||||
|
||||
await usefulBtn.trigger('click')
|
||||
|
||||
expect(usefulBtn.classes()).toContain('ring-2')
|
||||
expect(usefulBtn.classes()).toContain('bg-green-100')
|
||||
})
|
||||
|
||||
it('showNotesFieldOnPlusNote', async () => {
|
||||
const wrapper = mount(FeedbackButtons, { props: defaultProps })
|
||||
await ratingButton(wrapper, 'Useful').trigger('click')
|
||||
|
||||
const noteBtn = wrapper.findAll('button').find(b => b.text() === '+ Note')
|
||||
await noteBtn.trigger('click')
|
||||
|
||||
expect(wrapper.find('input').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('ariaCheckedOnSelectedRating', async () => {
|
||||
const wrapper = mount(FeedbackButtons, { props: defaultProps })
|
||||
const usefulBtn = ratingButton(wrapper, 'Useful')
|
||||
const fpBtn = ratingButton(wrapper, 'False positive')
|
||||
|
||||
await usefulBtn.trigger('click')
|
||||
|
||||
expect(usefulBtn.attributes('aria-checked')).toBe('true')
|
||||
expect(fpBtn.attributes('aria-checked')).toBe('false')
|
||||
})
|
||||
|
||||
it('keyboardNavigationWorks', async () => {
|
||||
const wrapper = mount(FeedbackButtons, { props: defaultProps })
|
||||
const buttons = ratingButtons(wrapper)
|
||||
|
||||
expect(buttons.every(b => b.element.tagName === 'BUTTON')).toBe(true)
|
||||
|
||||
await ratingButton(wrapper, 'Useful').trigger('click')
|
||||
expect(ratingButton(wrapper, 'Useful').attributes('aria-checked')).toBe('true')
|
||||
|
||||
await ratingButton(wrapper, 'Too early').trigger('click')
|
||||
expect(ratingButton(wrapper, 'Too early').attributes('aria-checked')).toBe('true')
|
||||
expect(ratingButton(wrapper, 'Useful').attributes('aria-checked')).toBe('false')
|
||||
|
||||
const noteBtn = wrapper.findAll('button').find(b => b.text() === '+ Note')
|
||||
await noteBtn.trigger('click')
|
||||
|
||||
const input = wrapper.get('input')
|
||||
await input.setValue('Expected after metoprolol')
|
||||
await input.trigger('keydown.enter')
|
||||
|
||||
expect(wrapper.find('input').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import FeedbackSummary from '@/views/FeedbackSummary.vue'
|
||||
import { useFeedbackStore } from '@/stores/feedback'
|
||||
|
||||
describe('FeedbackSummary', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
setActivePinia(createPinia())
|
||||
|
||||
const store = useFeedbackStore()
|
||||
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
|
||||
store.addFeedback('a2', 'SepsisWarning', 'Critical', 'would-act')
|
||||
store.addFeedback('a3', 'WarningHeartRate', 'Warning', 'false-positive')
|
||||
})
|
||||
|
||||
it('showsAggregateStats', () => {
|
||||
const wrapper = mount(FeedbackSummary)
|
||||
|
||||
expect(wrapper.text()).toContain('Total Ratings')
|
||||
expect(wrapper.text()).toContain('Useful / Would Act')
|
||||
expect(wrapper.text()).toContain('False Positive')
|
||||
expect(wrapper.text()).toContain('67%')
|
||||
expect(wrapper.text()).toContain('33%')
|
||||
})
|
||||
|
||||
it('showsPerAlertTypeBreakdown', () => {
|
||||
const wrapper = mount(FeedbackSummary)
|
||||
|
||||
expect(wrapper.text()).toContain('SEPSIS_WARNING')
|
||||
expect(wrapper.text()).toContain('WARNING_HEART_RATE')
|
||||
expect(wrapper.text()).toContain('2 ratings')
|
||||
expect(wrapper.text()).toContain('1 ratings')
|
||||
})
|
||||
|
||||
it('exportButtonsExist', () => {
|
||||
const wrapper = mount(FeedbackSummary)
|
||||
const buttons = wrapper.findAll('button')
|
||||
|
||||
expect(buttons.some(b => b.text().includes('Export JSON'))).toBe(true)
|
||||
expect(buttons.some(b => b.text().includes('Export CSV'))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useFeedbackStore } from '@/stores/feedback'
|
||||
|
||||
describe('useFeedbackStore', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('addFeedback_createsEntry', () => {
|
||||
const store = useFeedbackStore()
|
||||
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
|
||||
|
||||
expect(store.entries).toHaveLength(1)
|
||||
expect(store.entries[0].alertId).toBe('a1')
|
||||
expect(store.entries[0].rating).toBe('useful')
|
||||
})
|
||||
|
||||
it('addFeedback_updatesExisting', () => {
|
||||
const store = useFeedbackStore()
|
||||
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
|
||||
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'false-positive')
|
||||
|
||||
expect(store.entries).toHaveLength(1)
|
||||
expect(store.entries[0].rating).toBe('false-positive')
|
||||
})
|
||||
|
||||
it('stats_computesCorrectly', () => {
|
||||
const store = useFeedbackStore()
|
||||
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
|
||||
store.addFeedback('a2', 'SepsisWarning', 'Critical', 'would-act')
|
||||
store.addFeedback('a3', 'WarningHeartRate', 'Warning', 'false-positive')
|
||||
store.addFeedback('a4', 'WarningHeartRate', 'Warning', 'too-early')
|
||||
|
||||
expect(store.stats.total).toBe(4)
|
||||
expect(store.stats.useful).toBe(2)
|
||||
expect(store.stats.falsePositive).toBe(1)
|
||||
expect(store.stats.usefulPct).toBe(50)
|
||||
expect(store.stats.fpPct).toBe(25)
|
||||
})
|
||||
|
||||
it('byAlertType_groupsCorrectly', () => {
|
||||
const store = useFeedbackStore()
|
||||
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
|
||||
store.addFeedback('a2', 'WarningHeartRate', 'Warning', 'false-positive')
|
||||
store.addFeedback('a3', 'SepsisWarning', 'Critical', 'too-early')
|
||||
|
||||
expect(Object.keys(store.byAlertType)).toHaveLength(2)
|
||||
expect(store.byAlertType.SepsisWarning).toHaveLength(2)
|
||||
expect(store.byAlertType.WarningHeartRate).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('exportAsJson_generatesValidJson', () => {
|
||||
const store = useFeedbackStore()
|
||||
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
|
||||
|
||||
let capturedBlob = null
|
||||
const click = vi.fn()
|
||||
const originalCreateElement = document.createElement.bind(document)
|
||||
|
||||
URL.createObjectURL = vi.fn((blob) => {
|
||||
capturedBlob = blob
|
||||
return 'blob:url'
|
||||
})
|
||||
URL.revokeObjectURL = vi.fn()
|
||||
vi.spyOn(document, 'createElement').mockImplementation((tag) => {
|
||||
if (tag === 'a') return { href: '', download: '', click }
|
||||
return originalCreateElement(tag)
|
||||
})
|
||||
|
||||
expect(() => store.exportAsJson()).not.toThrow()
|
||||
expect(capturedBlob).toBeInstanceOf(Blob)
|
||||
expect(capturedBlob.type).toBe('application/json')
|
||||
expect(click).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clearAll_emptiesEntries', () => {
|
||||
const store = useFeedbackStore()
|
||||
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
|
||||
store.clearAll()
|
||||
|
||||
expect(store.entries).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('persistsToLocalStorage', () => {
|
||||
const store = useFeedbackStore()
|
||||
store.addFeedback('a1', 'SepsisWarning', 'Critical', 'useful')
|
||||
|
||||
const stored = JSON.parse(localStorage.getItem('vigilcare-feedback'))
|
||||
expect(stored).toHaveLength(1)
|
||||
expect(stored[0].rating).toBe('useful')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user