ios-projects/AIStudyApp/AIStudyAppTests/ActivityViewModelTests.swift
wangdl d8125ddf34 fix: iOS compile errors - protocol mismatches + void return
- ReadingRuntimeAdapter: clearExportedEvents is void, return count separately
- ActivityViewModel: FocusItemServicing.list→listAll, add days/type? params
- ActivityViewModelTests: update mocks to match new protocol signatures

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 16:58:23 +08:00

182 lines
6.8 KiB
Swift

import XCTest
@testable import AIStudyApp
// MARK: - Mock services
private final class MockActivityService: ActivityServicing {
var summary: ActivitySummary?
var heatmap: [String: Int] = [:]
var streak: ActivityStreak?
var trends: [ActivityTrend] = []
var recommendations: [ActivityRecommendation] = []
func summary() async throws -> ActivitySummary { try resultOrThrow(summary) }
func heatmap() async throws -> [String: Int] { heatmap }
func streak() async throws -> ActivityStreak { try resultOrThrow(streak) }
func trend(days: Int) async throws -> [ActivityTrend] { trends }
func recommendations() async throws -> [ActivityRecommendation] { recommendations }
}
private final class MockFocusItemService: FocusItemServicing {
var items: [FocusItem] = []
func listAll() async throws -> [FocusItem] { items }
}
private final class MockReadingAPIForActivity: ReadingContinueServicing & LearningStatusServicing {
var continueLearning: ContinueLearningResponse?
var learningSummary: LearningSummaryResponse?
var learningTrend: LearningTrendResponse?
var records: LearningRecordsResponse?
func getContinueLearning() async throws -> ContinueLearningResponse { try resultOrThrow(continueLearning) }
func getLearningSummary() async throws -> LearningSummaryResponse { try resultOrThrow(learningSummary) }
func getLearningTrend(days: Int) async throws -> LearningTrendResponse { try resultOrThrow(learningTrend) }
func getLearningRecords(limit: Int, type: String?) async throws -> LearningRecordsResponse { try resultOrThrow(records) }
}
private func resultOrThrow<T>(_ value: T?) throws -> T {
guard let v = value else { throw NSError(domain: "test", code: -1) }
return v
}
// MARK: - Tests
@MainActor
final class ActivityViewModelTests: XCTestCase {
var vm: ActivityViewModel!
var mockActivity: MockActivityService!
var mockFocus: MockFocusItemService!
var mockReading: MockReadingAPIForActivity!
override func setUp() {
super.setUp()
mockActivity = MockActivityService()
mockFocus = MockFocusItemService()
mockReading = MockReadingAPIForActivity()
vm = ActivityViewModel(
activityService: mockActivity,
focusService: mockFocus,
readingService: mockReading
)
}
override func tearDown() {
vm = nil
super.tearDown()
}
// MARK: - Initial state
func test_initial_isLoading_false() { XCTAssertFalse(vm.isLoading) }
func test_initial_errorMessage_nil() { XCTAssertNil(vm.errorMessage) }
func test_initial_summary_nil() { XCTAssertNil(vm.summary) }
func test_initial_learningSummary_nil() { XCTAssertNil(vm.learningSummary) }
func test_initial_readingTrend_nil() { XCTAssertNil(vm.readingTrend) }
// MARK: - loadAll() basic
func test_loadAll_sets_isLoading_false() async {
await vm.loadAll()
XCTAssertFalse(vm.isLoading)
}
func test_loadAll_sets_summary() async {
mockActivity.summary = ActivitySummary(totalMinutes: 60, totalCardsReviewed: 10, activeDays: 5, dailyAverage: 12)
await vm.loadAll()
XCTAssertEqual(vm.summary?.activeDays, 5)
}
func test_loadAll_errorMessage_when_summary_nil() async {
mockActivity.summary = nil
await vm.loadAll()
XCTAssertEqual(vm.errorMessage, "加载分析数据失败,请下拉刷新重试")
}
func test_loadAll_sets_focusItems() async {
mockFocus.items = [FocusItem(id: "f1", userId: nil, knowledgeBaseId: nil, knowledgeItemId: nil, title: "Focus", reason: nil, suggestion: nil, priority: nil, status: "active", masteryScore: nil)]
await vm.loadAll()
XCTAssertEqual(vm.focusItems.count, 1)
}
func test_loadAll_sets_heatmap() async {
mockActivity.heatmap = ["2026-01-01": 3]
await vm.loadAll()
XCTAssertEqual(vm.heatmap["2026-01-01"], 3)
}
func test_loadAll_sets_streak() async {
mockActivity.streak = ActivityStreak(currentStreak: 5, longestStreak: 10, lastActiveDate: "2026-01-01")
await vm.loadAll()
XCTAssertEqual(vm.streak?.currentStreak, 5)
}
func test_loadAll_sets_trends() async {
mockActivity.trends = [ActivityTrend(date: "2026-01-01", value: 8, label: "阅读")]
await vm.loadAll()
XCTAssertEqual(vm.trends.first?.label, "阅读")
}
func test_loadAll_sets_recommendations() async {
mockActivity.recommendations = [ActivityRecommendation(title: "继续学习", description: nil, type: "study", priority: nil)]
await vm.loadAll()
XCTAssertEqual(vm.recommendations.first?.title, "继续学习")
}
// MARK: - loadAll() learningSummary / trend
func test_loadAll_sets_learningSummary() async {
mockReading.learningSummary = LearningSummaryResponse(
todaySeconds: 120, weekSeconds: 600, totalSeconds: 3600,
activeDays: 5, sessionsCount: 10, materialsReadCount: 3,
markedReadCount: 1, dailyAverageSeconds: 600
)
await vm.loadAll()
XCTAssertEqual(vm.learningSummary?.todaySeconds, 120)
XCTAssertEqual(vm.learningSummary?.activeDays, 5)
}
func test_loadAll_sets_readingTrend() async {
mockReading.learningTrend = LearningTrendResponse(
days: 7,
series: [LearningTrendResponse.TrendPoint(date: "2026-01-01", value: 100)]
)
await vm.loadAll()
XCTAssertEqual(vm.readingTrend?.days, 7)
XCTAssertEqual(vm.readingTrend?.series.first?.value, 100)
}
func test_loadAll_sets_continueReading() async {
mockReading.continueLearning = ContinueLearningResponse(
type: "knowledge_source", materialId: "m1", title: "Book",
lastProgress: 0.5, totalActiveSeconds: 300, lastReadAt: "2026-01-01"
)
await vm.loadAll()
XCTAssertEqual(vm.continueReading?.materialId, "m1")
}
func test_loadAll_sets_recentRecords() async {
mockReading.records = LearningRecordsResponse(items: [
LearningRecordsResponse.RecordItem(id: "r1", recordType: "reading", title: "Rec1", description: nil, durationSeconds: 120, occurredAt: "2026-01-01")
], nextCursor: nil)
await vm.loadAll()
XCTAssertEqual(vm.recentRecords.count, 1)
XCTAssertEqual(vm.recentRecords.first?.title, "Rec1")
}
func test_loadAll_learningSummary_nil_on_error() async {
mockReading.learningSummary = nil
await vm.loadAll()
XCTAssertNil(vm.learningSummary)
}
// MARK: - refresh()
func test_refresh_clears_errorMessage() async {
vm.errorMessage = "old error"
mockActivity.summary = ActivitySummary(totalMinutes: 0, totalCardsReviewed: 0, activeDays: 0, dailyAverage: 0)
await vm.refresh()
XCTAssertNil(vm.errorMessage)
}
}