fix(review): 收敛进程分组键并补强日历测量测试
- 新增 ProcessUsage.rowGroupID,进程表与磁盘排行共用同一分组标识 - 弹窗额度读数只读取 AI 项目 - 合并预览栅格化的两处降级分支 - 日历测量测试改为方向性断言,去掉固定等待
This commit is contained in:
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)
|
||||
|
||||
Reference in new issue
Block a user