import Testing import SwiftData import Foundation @testable import 康康 struct CustomMonitorMetricTests { private func makeContainer() throws -> ModelContainer { let schema = Schema([ CustomMonitorMetric.self, Indicator.self, ]) let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) return try ModelContainer(for: schema, configurations: [config]) } @Test func seriesKeyAutoPrefixedWithCustom() { let m = CustomMonitorMetric(name: "腰围", unit: "cm") #expect(m.seriesKey.hasPrefix("custom.")) #expect(m.seriesKey.count > "custom.".count) } @Test func seriesKeyUniquePerInstance() { let a = CustomMonitorMetric(name: "腰围", unit: "cm") let b = CustomMonitorMetric(name: "腰围", unit: "cm") #expect(a.seriesKey != b.seriesKey) } @Test func referenceRangeNilWhenBoundsAbsentOrInverted() { let none = CustomMonitorMetric(name: "x", unit: "") #expect(none.referenceRange == nil) let inverted = CustomMonitorMetric(name: "x", unit: "", lowerBound: 100, upperBound: 50) #expect(inverted.referenceRange == nil) let valid = CustomMonitorMetric(name: "x", unit: "", lowerBound: 60, upperBound: 100) #expect(valid.referenceRange == 60...100) } @Test func rangeTextFormattingDropsTrailingZero() { let intRange = CustomMonitorMetric(name: "x", unit: "cm", lowerBound: 70, upperBound: 90) #expect(intRange.rangeText == "70 - 90") let decimalRange = CustomMonitorMetric(name: "y", unit: "kg", lowerBound: 60.5, upperBound: 65.5) #expect(decimalRange.rangeText == "60.5 - 65.5") } @Test func roundtripsThroughSwiftData() throws { let container = try makeContainer() let ctx = ModelContext(container) let m = CustomMonitorMetric(name: "腰围", unit: "cm", lowerBound: 70, upperBound: 90, icon: "flame.fill") ctx.insert(m) try ctx.save() let fetched = try #require(try ctx.fetch(FetchDescriptor()).first) #expect(fetched.name == "腰围") #expect(fetched.unit == "cm") #expect(fetched.lowerBound == 70) #expect(fetched.upperBound == 90) #expect(fetched.icon == "flame.fill") #expect(fetched.seriesKey.hasPrefix("custom.")) } @Test func seriesBucketResolvesCustomTitleAndRange() { let custom = CustomMonitorMetric(name: "腰围", unit: "cm", lowerBound: 70, upperBound: 90) let key = custom.seriesKey let now = Date() let day = { (offset: Int) -> Date in Calendar.current.date(byAdding: .day, value: offset, to: now)! } let items = [ Indicator(name: "腰围", value: "80", unit: "cm", range: "70-90", status: .normal, capturedAt: day(-2), seriesKey: key), Indicator(name: "腰围", value: "82", unit: "cm", range: "70-90", status: .normal, capturedAt: day(-1), seriesKey: key), ] let buckets = SeriesBucket.build(from: items, customMetrics: [custom]) #expect(buckets.count == 1) let b = try! #require(buckets.first) #expect(b.title == "腰围") #expect(b.unit == "cm") #expect(b.lines.first?.referenceRange == 70...90) } @Test func nameConflictEmptyNameYieldsNone() { let result = detectNameConflict(candidate: " ", customs: []) #expect(result == .none) } @Test func nameConflictDetectsBuiltinMatch() { let result = detectNameConflict(candidate: "血压", customs: []) #expect(result == .builtin("血压")) } @Test func nameConflictBuiltinIgnoresWhitespace() { let result = detectNameConflict(candidate: " 空腹血糖 ", customs: []) #expect(result == .builtin("空腹血糖")) } @Test func nameConflictDetectsExistingCustom() { let existing = CustomMonitorMetric(name: "腰围", unit: "cm") let result = detectNameConflict(candidate: "腰围", customs: [existing]) #expect(result == .existingCustom("腰围")) } @Test func nameConflictAllowsRenamingSelf() { // 编辑自己时,即使没改名也不应该报冲突 let me = CustomMonitorMetric(name: "腰围", unit: "cm") let result = detectNameConflict( candidate: "腰围", customs: [me], excludingSeriesKey: me.seriesKey ) #expect(result == .none) } @Test func nameConflictUnique() { let result = detectNameConflict(candidate: "步数", customs: []) #expect(result == .none) } @Test func seriesBucketFallsBackToIndicatorNameWhenCustomMissing() { // 用户删了 CustomMonitorMetric 但 Indicator 还在 → title fallback 到 indicator.name let orphanKey = "custom.deleted-xxxxx" let now = Date() let items = [ Indicator(name: "睡眠时长", value: "7", unit: "h", range: "", status: .normal, capturedAt: now, seriesKey: orphanKey), Indicator(name: "睡眠时长", value: "8", unit: "h", range: "", status: .normal, capturedAt: now.addingTimeInterval(60), seriesKey: orphanKey), ] let buckets = SeriesBucket.build(from: items, customMetrics: []) let b = try! #require(buckets.first) #expect(b.title == "睡眠时长") #expect(b.unit == "h") #expect(b.lines.first?.referenceRange == nil) } }