Look, I’ve been building systems that need to work when people are falling apart.
You can’t write a test for “user is having a breakdown.” But you can test whether your code notices the warning signs and actually helps.
This is my playbook for testing crisis features without turning humans into mock objects.
The Problem: You Can’t Mock Human Suffering
Unit tests verify code behavior. Trauma-informed features respond to human patterns breaking down in real time.
You can’t write this:
ts
// ❌ Doesn't exist
test('user having panic attack triggers support mode', () => {
const user = simulatePanicAttack();
expect(getSupportMode()).toBe(true);
});
But you can write this:
// ✅ Test the signals, not the human
test('error bursts plus help requests trigger escalation', () => {
for (let i = 0; i < 8; i++) crisis.trackError();
crisis.trackHelpRequest();
crisis.trackHelpRequest();
expect(crisis.level).toBe('moderate');
});
Rule one: test your interpretation of observable signals. The human experience stays human.
---
Strategy: Inject States, Don’t Simulate Trauma
Instead of faking distress, inject the system state that distress would change.
function TestWrapper({ children, crisis = {}, prefs = {} }) {
return (
<CrisisContext.Provider value={{ ...defaultCrisis, ...crisis }}>
<PrefsContext.Provider value={{ ...defaultPrefs, ...prefs }}>
{children}
</PrefsContext.Provider>
</CrisisContext.Provider>
);
}
Now you can test preference combinations safely:
it('high sensitivity catches subtle patterns', () => {
render(
<TestWrapper prefs={{ sensitivity: 'high' }}>
<CrisisDetector />
</TestWrapper>
);
expect(screen.getByTestId('threshold')).toHaveTextContent('6');
});
Don’t simulate fog. Inject the config your app would use during fog.
---
Generate Realistic Patterns, Not Random Noise
Your detection logic looks for patterns. Your test data better have patterns too.
function generatePainSeries(days, trend = 'stable') {
const entries = [];
for (let i = 0; i < days; i++) {
let intensity;
switch (trend) {
case 'worsening':
intensity = Math.min(10, 3 + (i / days) * 6);
break;
case 'improving':
intensity = Math.max(1, 9 - (i / days) * 5);
break;
case 'chaotic':
intensity = Math.max(1, Math.min(10, 5 + Math.sin(i * 0.8) * 4));
break;
default:
intensity = 5;
}
// NOTE: keep randomness deterministic if you want stable tests (seeded RNG)
entries.push({
date: new Date(Date.now() - (days - i) * 86400000),
pain: Math.round(intensity),
mood: intensity > 7 ? 2 : 7,
});
}
return entries;
}
Then test with realistic trajectories:
test('catches worsening trends before they crater', () => {
const data = generatePainSeries(14, 'worsening');
const analysis = analyzePattern(data);
expect(analysis.trend).toBe('deteriorating');
expect(analysis.shouldAlert).toBe(true);
});
Make your test data look like sessions, not academic examples.
---
Assert Outcomes, Not Internal Labels
Don’t test “system thinks user is stressed.” Test “system helps stressed user.”
test('emergency mode simplifies the interface outcomes', () => {
const { result } = renderHook(() => useCrisisMode(), { wrapper: TestWrapper });
act(() => result.current.activate('emergency'));
expect(result.current.prefs.touchTargets).toBe('large');
expect(result.current.prefs.confirmations).toBe('high');
expect(result.current.prefs.motion).toBe('reduced');
expect(result.current.prefs.contrast).toBe('high');
});
The question isn’t “did we detect crisis correctly?”
It’s “if someone’s in crisis, does this help?”
---
Time Matters: Use Fake Timers
Crisis detection happens over time: bursts, cooldowns, recovery windows.
describe('crisis timing', () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
test('requires sustained problems before escalating', () => {
const { result } = renderHook(() => useCrisisDetection());
// burst shouldn't escalate
act(() => {
for (let i = 0; i < 5; i++) result.current.trackError();
});
vi.advanceTimersByTime(5000);
expect(result.current.level).toBe('none');
// sustained pattern should
act(() => {
for (let i = 0; i < 8; i++) {
result.current.trackError();
vi.advanceTimersByTime(15000);
}
});
expect(result.current.level).not.toBe('none');
});
test('backs off after recovery period', () => {
const { result } = renderHook(() => useCrisisDetection());
act(() => result.current.forceLevel('moderate'));
vi.advanceTimersByTime(600000); // 10 minutes
expect(result.current.level).toBe('none');
});
});
Test activation. Test deactivation.
Apps that never calm down become part of the problem.
---
One More Thing: Test False Positives (Because “Help” Can Become Harm)
If your system escalates too easily, you create noise, anxiety, or UI friction. So I keep tests like:
“one bad minute shouldn’t trigger emergency mode”
“random flailing shouldn’t outrank a stable recovery trend”
“after recovery, don’t re-trigger instantly (hysteresis)”
This is where you prevent the system from becoming the boy who cried wolf.
---
Keep One Manual Dashboard
Unit tests catch regressions. Humans catch when something feels wrong.
I keep a quick crisis simulator dev tool to trigger states and visually verify the actual UI changes:
does simplified mode actually feel simpler?
are large touch targets actually easier to hit?
does emergency UI feel supportive, not alarming?
This isn’t automation. It’s a sanity check that the system still behaves like it gives a damn.
---
What Usually Goes Wrong
Testing emotional labels instead of behavioral signals and UI outcomes
Hardcoding thresholds instead of testing pattern recognition
Mocking the detection system until you’ve mocked away the actual risk
Only testing activation, never recovery
Assuming synchronous updates when you have debouncing and timeouts
---
Testing Hierarchy
Bottom: fixtures and generators for realistic scenarios
Middle: signal interpretation and threshold logic
Top: manual validation that this actually helps humans
Automated tests keep it mechanically sound. Human review keeps it humane.
---
The honest truth? You can’t fully automate empathy. But you can make your crisis detection reliable, your UI adaptations consistent, and your recovery paths safe.
When someone’s world is falling apart, your code better work the first time.
Next: what happens when the network dies during a crisis.
---
If you're struggling: In Canada, 9-8-8. In the US, 988.
Question: What do you test when building adaptive UX — signals, outcomes, false positives, or the full end-to-end session?
Top comments (0)