fix(review): 收敛进程分组键并补强日历测量测试

- 新增 ProcessUsage.rowGroupID,进程表与磁盘排行共用同一分组标识
- 弹窗额度读数只读取 AI 项目
- 合并预览栅格化的两处降级分支
- 日历测量测试改为方向性断言,去掉固定等待
This commit is contained in:
ysicing authored and 缘生 committed 2026-10-01 11:18:40 +08:00
1 parent 87079eddb2
commit 58544ecc0d
6 files changed
+43 -34

No files matched your search

@@ -325,16 +325,14 @@ enum MenuBarRenderer {
if let appearance { appearance.performAsCurrentDrawingAppearance(content) } else { content() }
}
let scale = backingScale ?? NSScreen.main?.backingScaleFactor ?? 2
guard let rep = NSBitmapImageRep(bitmapDataPlanes: nil,
pixelsWide: Int(ceil(image.size.width * scale)),
pixelsHigh: Int(ceil(image.size.height * scale)),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0) else {
return NSImage(size: image.size, flipped: false) { draw($0); return true }
}
let rep = NSBitmapImageRep(bitmapDataPlanes: nil,
pixelsWide: Int(ceil(image.size.width * scale)),
pixelsHigh: Int(ceil(image.size.height * scale)),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)
// 逻辑尺寸决定上下文的点到像素缩放,必须在创建上下文前设置。
rep.size = image.size
guard let context = NSGraphicsContext(bitmapImageRep: rep) else {
rep?.size = image.size
guard let rep, let context = NSGraphicsContext(bitmapImageRep: rep) else {
return NSImage(size: image.size, flipped: false) { draw($0); return true }
}
NSGraphicsContext.saveGraphicsState()
@@ -205,6 +205,11 @@ struct ProcessesPage: View {
// MARK: - 行模型
extension ProcessUsage {
/// 进程表的行标识:同一应用的进程合并为一行,其余按 PID 区分;磁盘排行沿用同一标识
var rowGroupID: String { appBundlePath.map { "app-\($0)" } ?? "pid-\(pid)" }
}
struct ProcessRowModel: Identifiable {
let id: String
let title: String
@@ -245,7 +250,7 @@ struct ProcessRowModel: Identifiable {
private init(group: [ProcessUsage], key: String, title: String) {
let representative = group.max { $0.memory < $1.memory } ?? group[0]
id = "app-\(key)"
id = key
self.title = title
subtitle = group.count > 1 ? tr("\(group.count) 个进程") : "PID \(representative.pid)"
bundlePath = representative.appBundlePath
@@ -271,7 +276,7 @@ struct ProcessRowModel: Identifiable {
static func grouped(_ processes: [ProcessUsage]) -> [ProcessRowModel] {
var buckets: [String: [ProcessUsage]] = [:]
for process in processes {
buckets[process.appBundlePath ?? "pid-\(process.pid)", default: []].append(process)
buckets[process.rowGroupID, default: []].append(process)
}
return buckets.map { key, group in
if let bundle = group[0].appBundlePath {
@@ -312,7 +312,7 @@ private struct OverviewReading {
}
case .aiUsage:
if !model.settings.aiUsageShowsLocalUsage {
if let quota = MenuBarReading(model: model).aiQuotas.min(by: { $0.remainingPercent < $1.remainingPercent }) {
if let quota = MenuBarReading(model: model, items: [.aiUsage]).aiQuotas.min(by: { $0.remainingPercent < $1.remainingPercent }) {
value = "\(quota.remainingPercent)"
unit = "%"
detail = quota.sourceName + " · " + tr("订阅额度")
@@ -109,11 +109,11 @@ public final class MetricsStore {
if let processes = snapshot.processes {
self.processes = processes
// 排行只需要磁盘计数,不必构建带名称、文案和其他指标的完整界面行。
// 分组键与 ProcessRowModel 保持一致;同组有读取计数时才采用写入合计,
// 保留混合权限进程和缺失计数的既有语义,并分别累加读写以保持浮点运算顺序。
// 同组有读取计数时才采用写入合计,保留混合权限进程和缺失计数的既有语义,
// 并分别累加读写以保持浮点运算顺序。
var totals: [String: (read: Double?, write: Double)] = [:]
for process in processes {
let id = process.appBundlePath.map { "app-\($0)" } ?? "pid-\(process.pid)"
let id = process.rowGroupID
var total = totals[id] ?? (nil, 0)
if let read = process.diskRead { total.read = (total.read ?? 0) + read }
total.write += process.diskWrite ?? 0
@@ -12,32 +12,37 @@ import Testing
struct CalendarEnhancementTests {
let zone = TimeZone(identifier: "Asia/Shanghai")!
@Test(arguments: [false, true], [false, true])
func detachedCalendarMeasurementIncludesEnabledContent(showsDayDetails: Bool, showsHolidayOverview: Bool) async throws {
@Test(arguments: [false, true])
func detachedCalendarMeasurementIncludesEnabledContent(showsDayDetails: Bool) throws {
let name = "CalendarMeasurementTests.\(UUID())"
let defaults = try #require(UserDefaults(suiteName: name))
defer { defaults.removePersistentDomain(forName: name) }
let settings = AppSettings(defaults: defaults, calendarLocale: Locale(identifier: "zh_CN"))
settings.calendarFeatures = [.lunar, .holidays, .festivals, .seasonalInfo]
settings.calendarPreferences.showHolidayOverview = showsHolidayOverview
let model = AppModel(settings: settings, historyURL: nil, aiUsageProviders: [], aiQuotaProviders: [])
let date = try #require(ISO8601DateFormatter().date(from: "2026-09-23T04:00:00Z"))
let hosting = NSHostingView(rootView: CalendarPopover(referenceDate: date, showsDayDetails: showsDayDetails,
firstWeekday: settings.calendarFirstWeekday)
.environment(model)
.environment(\.isSnapshot, true))
// StatusPanel 在挂载前测量;启用的卡片必须已计入高度,不能等 onAppear 后才补齐。
let measured = hosting.fittingSize
#expect(hosting.window == nil)
let window = NSWindow(contentRect: NSRect(origin: CGPoint(x: -10_000, y: -10_000), size: measured),
styleMask: .borderless, backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
defer { window.close() }
window.contentView = hosting
window.orderFrontRegardless()
hosting.layoutSubtreeIfNeeded()
try await Task.sleep(for: .milliseconds(150))
#expect(abs(hosting.fittingSize.height - measured.height) < 1)
// StatusPanel 在挂载前测量,此时 onAppear 尚未运行;启用的卡片必须已计入这次测量的高度。
let measuredHeight = { (showsHolidayOverview: Bool) -> CGFloat in
settings.calendarPreferences.showHolidayOverview = showsHolidayOverview
let hosting = NSHostingView(rootView: CalendarPopover(referenceDate: date, showsDayDetails: showsDayDetails,
firstWeekday: settings.calendarFirstWeekday)
.environment(model)
.environment(\.isSnapshot, true))
return hosting.fittingSize.height
}
let withoutOverview = measuredHeight(false)
let withOverview = measuredHeight(true)
if showsDayDetails {
// 详情页不显示假期概览;黄历宜忌须已计入,弹窗不能比单独的黄历卡片还矮。
#expect(withOverview == withoutOverview)
let day = try #require(CalendarEngine.today(at: date))
let almanac = NSHostingView(rootView: CalendarAlmanacView(day: day, almanac: CalendarEngine.almanac(for: day),
features: settings.calendarFeatures)
.frame(width: CalendarPopoverSizing.standard.width))
#expect(withOverview > almanac.fittingSize.height)
} else {
#expect(withOverview > withoutOverview)
}
}
@Test func calendarLayoutDoesNotFeedOverflowBackIntoWindowSize() async throws {
@@ -227,6 +227,7 @@ private func isolatedDefaults() -> UserDefaults {
let center = try #require(rep.colorAt(x: rep.pixelsWide / 2, y: rep.pixelsHigh / 2)?.usingColorSpace(.deviceRGB))
#expect(center.alphaComponent > 0.99)
#expect((center.redComponent > 0.5) == dark)
// 源图铺满整个画布,四角也应被着色;缩放错位时角落会留空或越界。
for (x, y) in [(0, 0), (rep.pixelsWide - 1, 0), (0, rep.pixelsHigh - 1), (rep.pixelsWide - 1, rep.pixelsHigh - 1)] {
let corner = try #require(rep.colorAt(x: x, y: y)?.usingColorSpace(.deviceRGB))
#expect(corner.alphaComponent > 0.99)