- 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>
182 lines
6.8 KiB
Swift
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)
|
|
}
|
|
}
|