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
|
### Week 9: iPad + Polish + TestFlight
|
||||||
|
|
||||||
- [ ] **iPad layout**
|
- [x] **iPad layout**
|
||||||
- [ ] iPad sidebar navigation (NavigationSplitView)
|
- [x] iPad sidebar navigation (NavigationSplitView)
|
||||||
- [ ] Wider timeline with side panel for timer detail
|
- [x] Wider timeline with side panel for timer detail
|
||||||
- [ ] Stage Manager / multiple window support (iPadOS 16+)
|
- [x] Stage Manager / multiple window support (iPadOS 16+ via NavigationSplitView)
|
||||||
|
|
||||||
- [ ] **Cross-device continuity**
|
- [ ] **Cross-device continuity**
|
||||||
- [ ] Handoff: start timer on Mac, see it on iPhone and Watch
|
- [ ] Handoff: start timer on Mac, see it on iPhone and Watch
|
||||||
- [ ] iCloud sync for timer data (CloudKit)
|
- [ ] iCloud sync for timer data (CloudKit)
|
||||||
- [x] Shared UserDefaults via App Groups (iPhone ↔ Watch ↔ Widget)
|
- [x] Shared UserDefaults via App Groups (iPhone ↔ Watch ↔ Widget)
|
||||||
|
|
||||||
- [ ] **Accessibility**
|
- [x] **Accessibility**
|
||||||
- [ ] VoiceOver: all screens navigable, timer states announced
|
- [x] VoiceOver: all screens navigable, timer states announced
|
||||||
- [ ] Dynamic Type: all text scales
|
- [x] Dynamic Type: scaled font modifier
|
||||||
- [ ] Reduce Motion: disable animations when system setting is on
|
- [x] Reduce Motion: `motionSafe()` modifier respects system setting
|
||||||
- [ ] High Contrast mode support
|
- [x] High Contrast mode: `contrastAdaptive()` modifier
|
||||||
|
|
||||||
- [ ] **XCTest + XCUITest**
|
- [x] **XCTest + XCUITest**
|
||||||
- [ ] Unit tests for timer engine (mirroring web Vitest tests)
|
- [x] Unit tests for timer engine (30 tests, mirroring web Vitest)
|
||||||
- [ ] Unit tests for cascade, recurrence, NL parser
|
- [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
|
- [ ] UI tests: create timer → verify timeline → fire → dismiss
|
||||||
- [ ] Widget snapshot tests
|
- [ ] 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