ios-projects/AIStudyApp/AIStudyAppTests/ReadingEventCollectorTests.swift
wangdl 4ba747af46 feat: iOS learning info P0 complete (IOS-INFO-000~017, 038~039)
Models: ReadingMaterialContext, ReadingEventUploadItem, AppContext
Runtime: ReadingRuntimeAdapter, SessionManager, RecoveryService
Reader: MaterialReaderView lifecycle, Heartbeat Timer, Position Adapter
Export: EventMapper, UploadQueue, export→queue→ack flow
Tests: 16 test files covering models, runtime, lifecycle, upload
Docs: ios-learning-info-architecture.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 21:53:36 +08:00

102 lines
3.6 KiB
Swift

import XCTest
@testable import AIStudyApp
final class ReadingEventCollectorTests: XCTestCase {
// MARK: - V1 Collector Lifecycle
func test_open_sets_activeMaterialId() {
let collector = ReadingEventCollector.shared
let previous = collector.lastPosition
collector.open(materialId: "mat-test-001")
// open should not crash, and should push a materialOpened event
// (verified by no crash; event count tested via export)
let events = collector.exportPending()
XCTAssertGreaterThanOrEqual(events.count, 1, "open should produce at least 1 event")
XCTAssertEqual(events.first?.materialId, "mat-test-001")
// Cleanup
collector.clearExported(events.count)
// Reset position to previous
if previous == nil {
// Nothing to restore
}
}
func test_close_returns_active_seconds() {
let collector = ReadingEventCollector.shared
collector.open(materialId: "mat-test-002")
// Simulate some reading time by opening and immediately closing
let seconds = collector.close(materialId: "mat-test-002")
XCTAssertGreaterThanOrEqual(seconds, 0, "close should return active seconds >= 0")
}
func test_close_on_unopened_material_does_not_crash() {
let collector = ReadingEventCollector.shared
// Closing a material that wasn't opened should not crash
let seconds = collector.close(materialId: "mat-never-opened")
XCTAssertEqual(seconds, 0, "close on unopened should return 0")
}
func test_mark_as_read_pushes_event() {
let collector = ReadingEventCollector.shared
collector.open(materialId: "mat-test-003")
let preCount = collector.exportPending().count
collector.clearExported(preCount)
collector.markAsRead(materialId: "mat-test-003")
let postEvents = collector.exportPending()
let hasMarkedAsRead = postEvents.contains { event in
if case .markedAsRead = event { return true }
return false
}
XCTAssertTrue(hasMarkedAsRead || !postEvents.isEmpty, "markAsRead should push an event")
// Cleanup
collector.clearExported(postEvents.count)
_ = collector.close(materialId: "mat-test-003")
let remaining = collector.exportPending()
collector.clearExported(remaining.count)
}
func test_duplicate_open_is_safe() {
let collector = ReadingEventCollector.shared
collector.open(materialId: "mat-test-004")
collector.open(materialId: "mat-test-004") // duplicate
// Should not crash, events should still be exportable
let events = collector.exportPending()
XCTAssertFalse(events.isEmpty)
collector.clearExported(events.count)
_ = collector.close(materialId: "mat-test-004")
let remaining = collector.exportPending()
collector.clearExported(remaining.count)
}
func test_lastPosition_updated() {
let collector = ReadingEventCollector.shared
collector.open(materialId: "mat-test-005")
let pos = ReadingPosition.markdown(blockId: "block-1", scrollProgress: 0.5)
collector.updatePosition(materialId: "mat-test-005", position: pos)
XCTAssertNotNil(collector.lastPosition, "lastPosition should be updated")
// Cleanup
let events = collector.exportPending()
collector.clearExported(events.count)
_ = collector.close(materialId: "mat-test-005")
let remaining = collector.exportPending()
collector.clearExported(remaining.count)
}
}