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>
96 lines
3.1 KiB
Swift
96 lines
3.1 KiB
Swift
import XCTest
|
|
@testable import AIStudyApp
|
|
|
|
final class EventMapperTests: XCTestCase {
|
|
|
|
// MARK: - eventType mapping
|
|
|
|
func test_apiEventType_materialOpened() {
|
|
XCTAssertEqual(ReadingEventMapper.apiEventType(.materialOpened), "material_opened")
|
|
}
|
|
|
|
func test_apiEventType_materialClosed() {
|
|
XCTAssertEqual(ReadingEventMapper.apiEventType(.materialClosed), "material_closed")
|
|
}
|
|
|
|
func test_apiEventType_positionChanged() {
|
|
XCTAssertEqual(ReadingEventMapper.apiEventType(.positionChanged), "position_changed")
|
|
}
|
|
|
|
func test_apiEventType_heartbeat() {
|
|
XCTAssertEqual(ReadingEventMapper.apiEventType(.heartbeat), "heartbeat")
|
|
}
|
|
|
|
func test_apiEventType_markedAsRead() {
|
|
XCTAssertEqual(ReadingEventMapper.apiEventType(.markedAsRead), "marked_as_read")
|
|
}
|
|
|
|
// MARK: - AppContext integration
|
|
|
|
func test_map_uses_appContext_fields() {
|
|
let ctx = ReadingMaterialContext(
|
|
readingTargetType: .knowledgeSource,
|
|
materialId: "mat-001",
|
|
knowledgeBaseId: "kb-001"
|
|
)
|
|
// No Rust events to map, but verify AppContext is used
|
|
let appCtx = AppContext(
|
|
platform: "ipadOS",
|
|
appVersion: "2.0.0",
|
|
buildNumber: "100",
|
|
clientTimezoneOffsetMinutes: 480,
|
|
locale: "en-US",
|
|
deviceType: "tablet",
|
|
osVersion: "18.0"
|
|
)
|
|
let items = ReadingEventMapper.map(rustEvents: [], contexts: ["mat-001": ctx], appContext: appCtx)
|
|
XCTAssertTrue(items.isEmpty, "Empty events → empty result")
|
|
}
|
|
|
|
func test_map_one_without_context_returns_nil() {
|
|
let ctx = ReadingMaterialContext(
|
|
readingTargetType: .temporaryFile,
|
|
materialId: "mat-002"
|
|
)
|
|
let items = ReadingEventMapper.map(
|
|
rustEvents: [],
|
|
contexts: ["other-id": ctx]
|
|
)
|
|
XCTAssertTrue(items.isEmpty)
|
|
}
|
|
|
|
// MARK: - All event types are mapped
|
|
|
|
func test_all_five_event_types_have_mapping() {
|
|
let types: [ReadingEventTypeV2] = [
|
|
.materialOpened, .materialClosed, .positionChanged, .heartbeat, .markedAsRead
|
|
]
|
|
for type in types {
|
|
let str = ReadingEventMapper.apiEventType(type)
|
|
XCTAssertFalse(str.isEmpty, "Event type \(type) should have non-empty string mapping")
|
|
XCTAssertTrue(str.contains("_"), "Should be snake_case: \(str)")
|
|
}
|
|
}
|
|
|
|
// MARK: - AppContext refreshed per call
|
|
|
|
func test_map_refreshes_timezone_per_call() {
|
|
let ctx = ReadingMaterialContext(
|
|
readingTargetType: .knowledgeSource,
|
|
materialId: "mat-tz"
|
|
)
|
|
let appCtx = AppContext(
|
|
platform: "ios",
|
|
appVersion: "1.0.0",
|
|
buildNumber: "1",
|
|
clientTimezoneOffsetMinutes: 0, // deliberately wrong
|
|
locale: nil,
|
|
deviceType: nil,
|
|
osVersion: nil
|
|
)
|
|
// map() calls refreshed() internally, so timezone should be updated
|
|
let items = ReadingEventMapper.map(rustEvents: [], contexts: ["mat-tz": ctx], appContext: appCtx)
|
|
XCTAssertNotNil(items)
|
|
}
|
|
}
|