test(week9): add urgency tests (14) + accessibility tests (14) + update roadmap Week 9
This commit is contained in:
parent
3bec3602d2
commit
2c224c5c4f
@ -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
|
||||
|
||||
|
||||
129
ios/ChronoMindTests/AccessibilityTests.swift
Normal file
129
ios/ChronoMindTests/AccessibilityTests.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
129
ios/ChronoMindTests/UrgencyTests.swift
Normal file
129
ios/ChronoMindTests/UrgencyTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user