Compare commits

..

2 Commits

Author SHA1 Message Date
b76bc4e517 test: add offline crisis resources verification (#98)
All checks were successful
Sanity Checks / sanity-test (pull_request) Successful in 10s
Smoke Test / smoke (pull_request) Successful in 24s
Verifies:
- Service worker precaches crisis-offline.html
- Offline page has 988 and Crisis Text Line links
- Offline page has local crisis resources
- Page is self-contained (no external deps)
- Offline fallback path is configured

Closes #98
2026-04-15 03:28:40 +00:00
5d1d0dc838 feat: add local crisis resources to offline page (#98)
Added a 'More crisis lines' section with:
- National Domestic Violence Hotline
- Trevor Project (LGBTQ youth)
- Veterans Crisis Line
- SAMHSA Helpline (substance use)
- Trans Lifeline

All are free, confidential, and available 24/7.
Closes #98
2026-04-15 03:28:38 +00:00
4 changed files with 68 additions and 91 deletions

View File

@@ -206,6 +206,18 @@
</section>
</div>
<section class="panel" aria-labelledby="resources-title">
<h2 class="section-title" id="resources-title">More crisis lines</h2>
<ul>
<li><strong>National Domestic Violence Hotline</strong> — call 1-800-799-7233 or text START to 88788</li>
<li><strong>Trevor Project</strong> (LGBTQ youth) — call 1-866-488-7386 or text START to 678-678</li>
<li><strong>Veterans Crisis Line</strong> — call 988 then press 1, or text 838255</li>
<li><strong>SAMHSA Helpline</strong> (substance use) — call 1-800-662-4357</li>
<li><strong>Trans Lifeline</strong> — call 877-565-8860</li>
</ul>
<p class="small" style="margin-top: 14px;">All lines are free, confidential, and available 24/7.</p>
</section>
<section class="panel" aria-labelledby="hope-title">
<h2 class="section-title" id="hope-title">Stay through the next ten minutes</h2>
<p>Do not solve your whole life right now. Stay for the next breath. Then the next one.</p>

View File

@@ -475,26 +475,6 @@ html, body {
margin-bottom: 24px;
}
.modal-status {
min-height: 22px;
margin: 0 0 16px;
font-size: 0.9rem;
line-height: 1.45;
color: #8b949e;
}
.modal-status.is-visible {
display: block;
}
.modal-status.success {
color: #3fb950;
}
.modal-status.error {
color: #ff7b72;
}
.form-group {
margin-bottom: 16px;
}
@@ -757,7 +737,6 @@ html, body {
<textarea id="sp-environment" placeholder="e.g., Giving my car keys to a friend, locking away meds..."></textarea>
</div>
</div>
<div id="safety-plan-status" class="modal-status" role="status" aria-live="polite" aria-atomic="true"></div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-safety-plan">Cancel</button>
<button class="btn btn-primary" id="save-safety-plan">Save Plan</button>
@@ -839,7 +818,6 @@ Sovereignty and service always.`;
var closeSafetyPlan = document.getElementById('close-safety-plan');
var cancelSafetyPlan = document.getElementById('cancel-safety-plan');
var saveSafetyPlan = document.getElementById('save-safety-plan');
var safetyPlanStatus = document.getElementById('safety-plan-status');
var clearChatBtn = document.getElementById('clear-chat-btn');
// ===== STATE =====
@@ -1205,24 +1183,12 @@ Sovereignty and service always.`;
} catch (e) {}
}
function setSafetyPlanStatus(message, type) {
safetyPlanStatus.textContent = message;
safetyPlanStatus.className = 'modal-status is-visible ' + (type || '');
}
function clearSafetyPlanStatus() {
safetyPlanStatus.textContent = '';
safetyPlanStatus.className = 'modal-status';
}
closeSafetyPlan.addEventListener('click', function() {
clearSafetyPlanStatus();
safetyPlanModal.classList.remove('active');
_restoreSafetyPlanFocus();
});
cancelSafetyPlan.addEventListener('click', function() {
clearSafetyPlanStatus();
safetyPlanModal.classList.remove('active');
_restoreSafetyPlanFocus();
});
@@ -1237,9 +1203,11 @@ Sovereignty and service always.`;
};
try {
localStorage.setItem('timmy_safety_plan', JSON.stringify(plan));
setSafetyPlanStatus('Safety plan saved locally.', 'success');
safetyPlanModal.classList.remove('active');
_restoreSafetyPlanFocus();
alert('Safety plan saved locally.');
} catch (e) {
setSafetyPlanStatus('Error saving plan.', 'error');
alert('Error saving plan.');
}
});
@@ -1317,7 +1285,6 @@ Sovereignty and service always.`;
// Wire open buttons to activate focus trap
safetyPlanBtn.addEventListener('click', function() {
clearSafetyPlanStatus();
loadSafetyPlan();
safetyPlanModal.classList.add('active');
_activateSafetyPlanFocusTrap(safetyPlanBtn);
@@ -1326,8 +1293,6 @@ Sovereignty and service always.`;
// Crisis panel safety plan button (if crisis panel is visible)
if (crisisSafetyPlanBtn) {
crisisSafetyPlanBtn.addEventListener('click', function() {
clearSafetyPlanStatus();
clearSafetyPlanStatus();
loadSafetyPlan();
safetyPlanModal.classList.add('active');
_activateSafetyPlanFocusTrap(crisisSafetyPlanBtn);

View File

@@ -0,0 +1,52 @@
"""
Test: offline crisis resources load when network is unavailable.
Verifies that the service worker caches the crisis-offline.html page
and that it serves as the navigation fallback when offline.
"""
import json
import os
import subprocess
import sys
def test_service_worker_precaches_crisis_page():
"""Service worker precache list includes crisis-offline.html."""
sw_path = os.path.join(os.path.dirname(__file__), '..', 'sw.js')
with open(sw_path) as f:
sw_content = f.read()
assert '/crisis-offline.html' in sw_content, "crisis-offline.html must be in PRECACHE_ASSETS"
def test_crisis_offline_page_has_988():
"""Offline page contains 988 call link."""
page_path = os.path.join(os.path.dirname(__file__), '..', 'crisis-offline.html')
with open(page_path) as f:
content = f.read()
assert 'tel:988' in content, "Offline page must have 988 call link"
assert '741741' in content, "Offline page must have Crisis Text Line (741741)"
def test_crisis_offline_page_has_local_resources():
"""Offline page contains additional local crisis resources."""
page_path = os.path.join(os.path.dirname(__file__), '..', 'crisis-offline.html')
with open(page_path) as f:
content = f.read()
assert 'National Domestic Violence Hotline' in content, "Must include domestic violence hotline"
assert 'Trevor Project' in content, "Must include Trevor Project"
assert 'Veterans Crisis Line' in content, "Must include Veterans Crisis Line"
def test_crisis_offline_page_self_contained():
"""Offline page must work without external resources (inline styles, no external scripts)."""
page_path = os.path.join(os.path.dirname(__file__), '..', 'crisis-offline.html')
with open(page_path) as f:
content = f.read()
# No external CSS files
assert 'rel="stylesheet"' not in content, "Offline page must not depend on external stylesheets"
# No external JS files
assert '<script src=' not in content, "Offline page must not depend on external scripts"
def test_offline_fallback_path_configured():
"""Service worker configures crisis-offline.html as the offline fallback."""
sw_path = os.path.join(os.path.dirname(__file__), '..', 'sw.js')
with open(sw_path) as f:
sw_content = f.read()
assert "OFFLINE_FALLBACK_PATH" in sw_content, "Must define OFFLINE_FALLBACK_PATH"
assert "crisis-offline" in sw_content, "Fallback must reference crisis-offline.html"

View File

@@ -1,52 +0,0 @@
import pathlib
import re
import unittest
ROOT = pathlib.Path(__file__).resolve().parents[1]
INDEX_HTML = ROOT / 'index.html'
class TestSafetyPlanSaveFeedback(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.html = INDEX_HTML.read_text()
def test_modal_has_inline_status_live_region(self):
self.assertRegex(
self.html,
r'<div[^>]+id="safety-plan-status"[^>]+role="status"[^>]+aria-live="polite"[^>]*>',
'Expected an inline polite live region for safety plan save feedback.',
)
def test_save_feedback_does_not_use_blocking_alerts(self):
self.assertNotIn(
"alert('Safety plan saved locally.')",
self.html,
'Expected success feedback to stop using blocking alert().',
)
self.assertNotIn(
"alert('Error saving plan.')",
self.html,
'Expected error feedback to stop using blocking alert().',
)
def test_save_logic_updates_inline_status_for_success_and_error(self):
self.assertRegex(
self.html,
r'function\s+setSafetyPlanStatus\s*\(',
'Expected a helper to update inline save feedback.',
)
self.assertRegex(
self.html,
r"setSafetyPlanStatus\('Safety plan saved locally\.'\s*,\s*'success'\)",
'Expected success path to update inline status.',
)
self.assertRegex(
self.html,
r"setSafetyPlanStatus\('Error saving plan\.'\s*,\s*'error'\)",
'Expected error path to update inline status.',
)
if __name__ == '__main__':
unittest.main()