Files
jayjennings 61c876edec
CI / Lint, typecheck, test, build (push) Canceled after 0s
CI / End-to-end tests (push) Canceled after 0s
Add OSHA Form 301 and Osha page components
- Implemented Form301 component for displaying OSHA Form 301 equivalent, including employee and case information.
- Created Osha component to manage OSHA recordkeeping, including tabs for 300 Log, 300A Summary, Hours & rates, and Submit to OSHA.
- Added functionality for printing and downloading CSV files for OSHA submissions.
- Integrated loading and error handling states for data fetching.
2026-10-06 13:09:57 -07:00

123 lines
5.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { assessRecordability, capDays, FIRST_AID, incidenceRate, itaSizeCode, severeInjuryReport, summaryChecks, type RecordabilityInput, type SummaryTotals } from './osha.ts'
const base: RecordabilityInput = {
employmentType: 'employee',
workRelated: true,
newCase: true,
died: false,
daysAwayFromWork: false,
restrictedOrTransfer: false,
medicalTreatment: false,
lossOfConsciousness: false,
significantDiagnosis: false,
specialCase: 'none',
}
const assess = (overrides: Partial<RecordabilityInput>) => assessRecordability({ ...base, ...overrides })
describe('assessRecordability', () => {
it('first aid only is not recordable', () => {
expect(assess({})).toMatchObject({ status: 'not_recordable', outcome: null })
})
it('waits for the work-related and new-case answers', () => {
expect(assess({ workRelated: null, medicalTreatment: true }).status).toBe('incomplete')
expect(assess({ newCase: null, medicalTreatment: true }).status).toBe('incomplete')
})
it('excludes non-work-related cases and recurrences', () => {
expect(assess({ workRelated: false, died: true }).status).toBe('not_recordable')
expect(assess({ newCase: false, medicalTreatment: true }).reasons[0]).toMatch(/1904\.6/)
})
it('keeps contractors and visitors off your log', () => {
expect(assess({ employmentType: 'contractor', daysAwayFromWork: true }).reasons[0]).toMatch(/1904\.31/)
expect(assess({ employmentType: 'visitor', died: true }).status).toBe('not_recordable')
expect(assess({ employmentType: 'temporary', medicalTreatment: true }).status).toBe('recordable')
})
it('classifies by the most serious outcome', () => {
expect(assess({ medicalTreatment: true }).outcome).toBe('other')
expect(assess({ medicalTreatment: true, restrictedOrTransfer: true }).outcome).toBe('restricted')
expect(assess({ restrictedOrTransfer: true, daysAwayFromWork: true }).outcome).toBe('days_away')
expect(assess({ daysAwayFromWork: true, died: true }).outcome).toBe('death')
})
it('records each general criterion on its own', () => {
for (const key of ['lossOfConsciousness', 'significantDiagnosis', 'medicalTreatment'] as const) {
expect(assess({ [key]: true })).toMatchObject({ status: 'recordable', outcome: 'other' })
}
})
it('records special cases even without general criteria', () => {
for (const specialCase of ['needlestick', 'medical_removal', 'hearing_loss', 'tuberculosis'] as const) {
expect(assess({ specialCase }).status).toBe('recordable')
}
expect(assess({ specialCase: 'needlestick' }).reasons[0]).toMatch(/1904\.8/)
})
it('lists every reason that applies', () => {
expect(assess({ medicalTreatment: true, lossOfConsciousness: true }).reasons).toHaveLength(2)
})
})
describe('severeInjuryReport', () => {
it('requires fatalities within 8 hours and other severe injuries within 24', () => {
expect(severeInjuryReport({ died: true, inpatientHospitalization: true, amputation: false, lossOfEye: false })).toEqual({ withinHours: 8, reasons: ['work-related fatality'] })
expect(severeInjuryReport({ died: false, inpatientHospitalization: false, amputation: true, lossOfEye: true })).toEqual({ withinHours: 24, reasons: ['amputation', 'loss of an eye'] })
expect(severeInjuryReport({ died: false, inpatientHospitalization: false, amputation: false, lossOfEye: false })).toBeNull()
})
})
describe('capDays', () => {
it('stops counting at 180 days in total', () => {
expect(capDays(10, 5)).toEqual({ daysAway: 10, daysRestricted: 5 })
expect(capDays(150, 60)).toEqual({ daysAway: 150, daysRestricted: 30 })
expect(capDays(200, 10)).toEqual({ daysAway: 180, daysRestricted: 0 })
})
})
describe('rates and codes', () => {
it('computes incidence rates per 200,000 hours', () => {
expect(incidenceRate(3, 200_000)).toBe(3)
expect(incidenceRate(2, 150_000)).toBe(2.67)
expect(incidenceRate(1, 0)).toBeNull()
})
it('maps headcount to ITA size codes', () => {
expect([19, 20, 99, 100, 249, 250].map(itaSizeCode)).toEqual([1, 21, 21, 22, 22, 3])
})
it('has the complete 14-item first-aid list', () => {
expect(FIRST_AID).toHaveLength(14)
})
})
describe('summaryChecks (OSHA ITA validations)', () => {
const totals = (overrides: Partial<SummaryTotals> = {}): SummaryTotals => ({
deaths: 0,
daysAwayCases: 1,
restrictedCases: 1,
otherCases: 1,
daysAway: 5,
daysRestricted: 3,
byType: { injury: 3, skin_disorder: 0, respiratory: 0, poisoning: 0, hearing_loss: 0, other_illness: 0 },
...overrides,
})
it('passes a consistent summary', () => {
expect(summaryChecks(totals(), 50, 100_000)).toEqual({ errors: [], warnings: [] })
})
it('catches the cross-field rules OSHA enforces', () => {
expect(summaryChecks(totals({ byType: { ...totals().byType, injury: 2 } }), 50, 100_000).errors[0]).toMatch(/add up to 2/)
expect(summaryChecks(totals({ daysAway: 0 }), 50, 100_000).errors).toContain('There are days-away cases (H) but no days away (K).')
expect(summaryChecks(totals({ restrictedCases: 0, daysAwayCases: 0, otherCases: 3, daysAway: 0 }), 50, 100_000).errors).toContain(
'There are restricted days (L) but no days-away or restriction cases (H or I).',
)
expect(summaryChecks(totals(), 0, 0).errors).toHaveLength(2)
expect(summaryChecks(totals(), 10, 100_000).errors[0]).toMatch(/more than the hours in a year/)
expect(summaryChecks(totals(), 50, 20_000).warnings[0]).toMatch(/Fewer than 500/)
})
})