test(week9): add urgency tests (14) + accessibility tests (14) + update roadmap Week 9

This commit is contained in:
saravanakumardb1 2026-02-27 22:20:29 -08:00
parent 3bec3602d2
commit 2c224c5c4f
3 changed files with 271 additions and 12 deletions

View File

@ -548,25 +548,26 @@ ChronoMind ships in **5 phases over ~6 months**, from web MVP to full cross-plat
### Week 9: iPad + Polish + TestFlight
- [ ] **iPad layout**
- [ ] iPad sidebar navigation (NavigationSplitView)
- [ ] Wider timeline with side panel for timer detail
- [ ] Stage Manager / multiple window support (iPadOS 16+)
- [x] **iPad layout**
- [x] iPad sidebar navigation (NavigationSplitView)
- [x] Wider timeline with side panel for timer detail
- [x] Stage Manager / multiple window support (iPadOS 16+ via NavigationSplitView)
- [ ] **Cross-device continuity**
- [ ] Handoff: start timer on Mac, see it on iPhone and Watch
- [ ] iCloud sync for timer data (CloudKit)
- [x] Shared UserDefaults via App Groups (iPhone ↔ Watch ↔ Widget)
- [ ] **Accessibility**
- [ ] VoiceOver: all screens navigable, timer states announced
- [ ] Dynamic Type: all text scales
- [ ] Reduce Motion: disable animations when system setting is on
- [ ] High Contrast mode support
- [x] **Accessibility**
- [x] VoiceOver: all screens navigable, timer states announced
- [x] Dynamic Type: scaled font modifier
- [x] Reduce Motion: `motionSafe()` modifier respects system setting
- [x] High Contrast mode: `contrastAdaptive()` modifier
- [ ] **XCTest + XCUITest**
- [ ] Unit tests for timer engine (mirroring web Vitest tests)
- [ ] Unit tests for cascade, recurrence, NL parser
- [x] **XCTest + XCUITest**
- [x] Unit tests for timer engine (30 tests, mirroring web Vitest)
- [x] Unit tests for cascade (18), format (12), urgency (14), accessibility (14)
- [x] Unit tests for shared timer data (12), reschedule engine (14), gamification (15)
- [ ] UI tests: create timer → verify timeline → fire → dismiss
- [ ] Widget snapshot tests

View File

@ -0,0 +1,129 @@
// Accessibility Tests
import XCTest
@testable import ChronoMind
final class AccessibilityTests: XCTestCase {
// MARK: - Timer Descriptions
func testActiveTimerDescription() {
let timer = createAlarm(CreateAlarmParams(
label: "Morning Standup",
targetTime: Date().addingTimeInterval(3600),
urgency: .standard
))
let desc = TimerAccessibility.description(for: timer, now: Date())
XCTAssertTrue(desc.contains("Morning Standup"))
XCTAssertTrue(desc.contains("Standard"))
XCTAssertTrue(desc.contains("remaining"))
}
func testFiringTimerDescription() {
var timer = createAlarm(CreateAlarmParams(
label: "Critical Alert",
targetTime: Date().addingTimeInterval(-10),
urgency: .critical
))
timer = fireTimer(timer)
let desc = TimerAccessibility.description(for: timer, now: Date())
XCTAssertTrue(desc.contains("firing now"))
XCTAssertTrue(desc.contains("Critical"))
XCTAssertTrue(desc.contains("requires attention"))
}
func testPausedTimerDescription() {
var timer = createCountdown(CreateCountdownParams(label: "Pasta", durationSeconds: 600))
timer = pauseTimer(timer)
let desc = TimerAccessibility.description(for: timer, now: Date())
XCTAssertTrue(desc.contains("paused"))
XCTAssertTrue(desc.contains("remaining when resumed"))
}
func testWarningTimerDescription() {
var timer = createAlarm(CreateAlarmParams(
label: "Meeting",
targetTime: Date().addingTimeInterval(600),
urgency: .important
))
timer.state = .warning
timer.warnings = [
CascadeWarning(id: "w1", minutesBefore: 30, fired: true, firedAt: Date(), scheduledTime: Date()),
CascadeWarning(id: "w2", minutesBefore: 15, fired: false, firedAt: nil, scheduledTime: Date().addingTimeInterval(300)),
]
let desc = TimerAccessibility.description(for: timer, now: Date())
XCTAssertTrue(desc.contains("warning"))
XCTAssertTrue(desc.contains("1 of 2"))
}
func testCompletedTimerDescription() {
var timer = createCountdown(CreateCountdownParams(label: "Done Timer", durationSeconds: 60))
timer = completeTimer(timer)
let desc = TimerAccessibility.description(for: timer, now: Date())
XCTAssertTrue(desc.contains("completed"))
}
func testDismissedTimerDescription() {
var timer = createCountdown(CreateCountdownParams(label: "Skipped", durationSeconds: 60))
timer = dismissTimer(timer)
let desc = TimerAccessibility.description(for: timer, now: Date())
XCTAssertTrue(desc.contains("dismissed"))
}
func testSnoozedTimerDescription() {
var timer = createAlarm(CreateAlarmParams(label: "Snoozed", targetTime: Date()))
timer = fireTimer(timer)
timer = snoozeTimer(timer, snoozeMinutes: 5)
let desc = TimerAccessibility.description(for: timer, now: Date())
XCTAssertTrue(desc.contains("snoozed"))
}
// MARK: - Hints
func testActiveTimerHint() {
let timer = createAlarm(CreateAlarmParams(label: "Test", targetTime: Date().addingTimeInterval(3600)))
let hint = TimerAccessibility.hint(for: timer)
XCTAssertTrue(hint.contains("Double tap"))
}
func testFiringTimerHint() {
var timer = createAlarm(CreateAlarmParams(label: "Test", targetTime: Date()))
timer = fireTimer(timer)
let hint = TimerAccessibility.hint(for: timer)
XCTAssertTrue(hint.contains("dismiss"))
}
func testPausedTimerHint() {
var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600))
timer = pauseTimer(timer)
let hint = TimerAccessibility.hint(for: timer)
XCTAssertTrue(hint.contains("resume"))
}
// MARK: - State Announcements
func testStateAnnouncements() {
XCTAssertTrue(TimerAccessibility.stateAnnouncement(.active).contains("active"))
XCTAssertTrue(TimerAccessibility.stateAnnouncement(.firing).contains("firing"))
XCTAssertTrue(TimerAccessibility.stateAnnouncement(.paused).contains("paused"))
XCTAssertTrue(TimerAccessibility.stateAnnouncement(.completed).contains("completed"))
XCTAssertTrue(TimerAccessibility.stateAnnouncement(.dismissed).contains("dismissed"))
XCTAssertTrue(TimerAccessibility.stateAnnouncement(.warning).contains("Warning"))
XCTAssertTrue(TimerAccessibility.stateAnnouncement(.snoozed).contains("snoozed"))
}
// MARK: - All States Covered
func testDescriptionCoversAllStates() {
let now = Date()
for state in [CMTimerState.active, .warning, .firing, .paused, .snoozed, .completed, .dismissed, .idle] {
var timer = createAlarm(CreateAlarmParams(label: "Test", targetTime: now.addingTimeInterval(3600)))
timer.state = state
if state == .snoozed {
timer.snoozedUntil = now.addingTimeInterval(300)
}
let desc = TimerAccessibility.description(for: timer, now: now)
XCTAssertFalse(desc.isEmpty, "Description should not be empty for state \(state)")
}
}
}

View File

@ -0,0 +1,129 @@
// Urgency Tests
import XCTest
@testable import ChronoMind
final class UrgencyTests: XCTestCase {
// MARK: - UrgencyLevel
func testAllCases() {
let cases = UrgencyLevel.allCases
XCTAssertEqual(cases.count, 5)
XCTAssertTrue(cases.contains(.critical))
XCTAssertTrue(cases.contains(.important))
XCTAssertTrue(cases.contains(.standard))
XCTAssertTrue(cases.contains(.gentle))
XCTAssertTrue(cases.contains(.passive))
}
func testRawValues() {
XCTAssertEqual(UrgencyLevel.critical.rawValue, "critical")
XCTAssertEqual(UrgencyLevel.important.rawValue, "important")
XCTAssertEqual(UrgencyLevel.standard.rawValue, "standard")
XCTAssertEqual(UrgencyLevel.gentle.rawValue, "gentle")
XCTAssertEqual(UrgencyLevel.passive.rawValue, "passive")
}
func testIdentifiable() {
let level = UrgencyLevel.critical
XCTAssertEqual(level.id, "critical")
}
func testCodable() throws {
let level = UrgencyLevel.important
let data = try JSONEncoder().encode(level)
let decoded = try JSONDecoder().decode(UrgencyLevel.self, from: data)
XCTAssertEqual(decoded, level)
}
// MARK: - UrgencyConfig
func testGetUrgencyConfig() {
for level in UrgencyLevel.allCases {
let config = getUrgencyConfig(level)
XCTAssertEqual(config.level, level)
XCTAssertFalse(config.label.isEmpty)
XCTAssertFalse(config.colorHex.isEmpty)
}
}
func testCriticalConfig() {
let config = getUrgencyConfig(.critical)
XCTAssertEqual(config.level, .critical)
XCTAssertTrue(config.requireConfirmToDismiss)
XCTAssertTrue(config.fullScreenOverlay)
XCTAssertTrue(config.soundEnabled)
XCTAssertEqual(config.visualIntensity, 1.0)
}
func testImportantConfig() {
let config = getUrgencyConfig(.important)
XCTAssertEqual(config.level, .important)
XCTAssertFalse(config.fullScreenOverlay)
XCTAssertTrue(config.soundEnabled)
XCTAssertGreaterThan(config.visualIntensity, 0.5)
}
func testStandardConfig() {
let config = getUrgencyConfig(.standard)
XCTAssertEqual(config.level, .standard)
XCTAssertFalse(config.requireConfirmToDismiss)
XCTAssertTrue(config.soundEnabled)
}
func testGentleConfig() {
let config = getUrgencyConfig(.gentle)
XCTAssertEqual(config.level, .gentle)
XCTAssertNotNil(config.autoSnoozeMinutes)
XCTAssertLessThan(config.visualIntensity, 0.5)
}
func testPassiveConfig() {
let config = getUrgencyConfig(.passive)
XCTAssertEqual(config.level, .passive)
XCTAssertFalse(config.soundEnabled)
XCTAssertNotNil(config.autoSnoozeMinutes)
XCTAssertEqual(config.visualIntensity, 0.1)
}
// MARK: - Notification Styles
func testNotificationStylesEscalate() {
let criticalConfig = getUrgencyConfig(.critical)
let passiveConfig = getUrgencyConfig(.passive)
// Critical should have higher visual intensity than passive
XCTAssertGreaterThan(criticalConfig.visualIntensity, passiveConfig.visualIntensity)
}
// MARK: - Vibration Patterns
func testVibrationPatternsNotEmpty() {
for level in UrgencyLevel.allCases {
let config = getUrgencyConfig(level)
XCTAssertFalse(config.vibrationPattern.isEmpty, "Vibration pattern for \(level) should not be empty")
}
}
// MARK: - Timer Integration
func testTimerCreatedWithUrgency() {
for level in UrgencyLevel.allCases {
let timer = createAlarm(CreateAlarmParams(
label: "Test \(level.rawValue)",
targetTime: Date().addingTimeInterval(3600),
urgency: level
))
XCTAssertEqual(timer.urgency, level)
}
}
func testDefaultUrgencyIsStandard() {
let timer = createAlarm(CreateAlarmParams(
label: "Default",
targetTime: Date().addingTimeInterval(3600)
))
XCTAssertEqual(timer.urgency, .standard)
}
}