From 5530d1b368faa6bc33915a6bc91f23ea1dda15fe Mon Sep 17 00:00:00 2001 From: nsd97 Date: Sun, 19 Jul 2026 10:10:12 -0400 Subject: [PATCH] Add Swift Testing harness and SwiftUI previews for all views. Introduce PreviewSupport fixtures (no SMC/XPC in canvas), #Preview on every View, and a ChillMacTests target so previews and fixture contracts stay verifiable. Co-authored-by: Cursor --- .github/PULL_REQUEST_TEMPLATE.md | 1 + AGENTS.md | 15 +- CLAUDE.md | 15 +- CONTRIBUTING.md | 5 +- ChillMac/App/DetailPanelController.swift | 10 + ChillMac/App/Theme.swift | 11 ++ ChillMac/Fan/SystemInfo.swift | 25 +++ ChillMac/Preview/PreviewSupport.swift | 179 ++++++++++++++++++ ChillMac/Views/BatteryDetailView.swift | 25 +++ ChillMac/Views/CpuDetailView.swift | 43 +++++ ChillMac/Views/DiskDetailView.swift | 31 +++ ChillMac/Views/FanRowView.swift | 24 +++ ChillMac/Views/MemoryDetailView.swift | 23 +++ ChillMac/Views/PanelResizeHandle.swift | 12 ++ ChillMac/Views/PopoverView.swift | 62 ++++++ ChillMac/Views/SettingsView.swift | 33 ++++ ChillMac/Views/TemperatureDetailView.swift | 18 ++ ChillMac/Views/TemperatureRowView.swift | 15 ++ ChillMacTests/Fixtures/.gitkeep | 0 .../Fixtures/PreviewSupportTests.swift | 33 ++++ ChillMacTests/Support/.gitkeep | 0 ChillMacTests/Support/HarnessSmokeTests.swift | 5 + TestPlans/Unit.xctestplan | 29 +++ project.yml | 29 +++ 24 files changed, 637 insertions(+), 6 deletions(-) create mode 100644 ChillMac/Preview/PreviewSupport.swift create mode 100644 ChillMacTests/Fixtures/.gitkeep create mode 100644 ChillMacTests/Fixtures/PreviewSupportTests.swift create mode 100644 ChillMacTests/Support/.gitkeep create mode 100644 ChillMacTests/Support/HarnessSmokeTests.swift create mode 100644 TestPlans/Unit.xctestplan diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4899a2f..2480f6e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,4 +7,5 @@ ## Checklist - [ ] Builds without errors (`xcodegen generate && xcodebuild -scheme ChillMac build`) +- [ ] Unit tests pass (`xcodebuild -project ChillMac.xcodeproj -scheme ChillMac -destination 'platform=macOS' test`) - [ ] Tested manually in the app diff --git a/AGENTS.md b/AGENTS.md index 469cd75..d9b45fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,6 +93,7 @@ ChillMac/ BatteryInfo Battery info via IOKit/IOPowerSources (polls every 5s) SMC/ - IOKit bridge (SMCConnection, SMCTypes, SMCKeys) XPC/ - HelperConnection (client), HelperInstaller + Preview/ - PreviewSupport factories for SwiftUI canvas + tests (#if DEBUG) FanControlHelper/ main.swift - Helper daemon entry point HelperDelegate.swift - XPC listener + code signature validation @@ -123,6 +124,16 @@ scripts/ - 420x640 main popover, 370x560 detail panels - Footer with quit button, app name, and °F/°C toggle -## No Tests +## Testing -There is no test suite. Testing is done manually via the UI. +Unit tests live in `ChillMacTests` (Swift Testing). Run: + +```bash +xcodegen generate && xcodebuild -project ChillMac.xcodeproj -scheme ChillMac -destination 'platform=macOS' test +``` + +- Use `@Test` / `#expect` / `@Suite` (Swift Testing only — no XCTest UI suite yet) +- Sample data for fixtures and canvas comes from `ChillMac/Preview/PreviewSupport.swift` — do not hit live SMC/XPC in unit tests or invent local sample data in views +- SwiftUI convention: add in-file `#Preview("…")` (plain macros, no traits) wired to `PreviewSupport` factories; keep `#Preview` under `#if DEBUG` +- Zero external test dependencies (no ViewInspector, no snapshot libs) +- Test plan: `TestPlans/Unit.xctestplan` diff --git a/CLAUDE.md b/CLAUDE.md index 469cd75..d9b45fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,6 +93,7 @@ ChillMac/ BatteryInfo Battery info via IOKit/IOPowerSources (polls every 5s) SMC/ - IOKit bridge (SMCConnection, SMCTypes, SMCKeys) XPC/ - HelperConnection (client), HelperInstaller + Preview/ - PreviewSupport factories for SwiftUI canvas + tests (#if DEBUG) FanControlHelper/ main.swift - Helper daemon entry point HelperDelegate.swift - XPC listener + code signature validation @@ -123,6 +124,16 @@ scripts/ - 420x640 main popover, 370x560 detail panels - Footer with quit button, app name, and °F/°C toggle -## No Tests +## Testing -There is no test suite. Testing is done manually via the UI. +Unit tests live in `ChillMacTests` (Swift Testing). Run: + +```bash +xcodegen generate && xcodebuild -project ChillMac.xcodeproj -scheme ChillMac -destination 'platform=macOS' test +``` + +- Use `@Test` / `#expect` / `@Suite` (Swift Testing only — no XCTest UI suite yet) +- Sample data for fixtures and canvas comes from `ChillMac/Preview/PreviewSupport.swift` — do not hit live SMC/XPC in unit tests or invent local sample data in views +- SwiftUI convention: add in-file `#Preview("…")` (plain macros, no traits) wired to `PreviewSupport` factories; keep `#Preview` under `#if DEBUG` +- Zero external test dependencies (no ViewInspector, no snapshot libs) +- Test plan: `TestPlans/Unit.xctestplan` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ae285c..e55c2b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ Debug builds skip the helper's code signature check, so you don't need a Develop 1. Fork the repo and create a branch from `main` 2. Make your changes 3. Make sure it builds: `xcodegen generate && xcodebuild -scheme ChillMac build` -4. Test manually — there's no automated test suite +4. Run unit tests: `xcodebuild -project ChillMac.xcodeproj -scheme ChillMac -destination 'platform=macOS' test` 5. Open a PR ## Project Overview @@ -40,7 +40,8 @@ Debug builds skip the helper's code signature check, so you don't need a Develop - Match the existing code style (no linter configured, just stay consistent) - SwiftUI views go in `ChillMac/Views/` - New monitors/data models go in `ChillMac/Fan/` -- The app has zero external dependencies — let's keep it that way unless there's a strong reason +- Use `PreviewSupport` for sample data; add in-file `#Preview` (no live SMC/XPC in canvas) +- The app has zero external dependencies — let's keep it that way unless there's a strong reason (Swift Testing only; no third-party test libs) ## Architecture Notes diff --git a/ChillMac/App/DetailPanelController.swift b/ChillMac/App/DetailPanelController.swift index f7cc55f..c0b8bf6 100644 --- a/ChillMac/App/DetailPanelController.swift +++ b/ChillMac/App/DetailPanelController.swift @@ -166,3 +166,13 @@ struct DetailPanelCloseButton: View { .buttonStyle(.plain) } } + +#if DEBUG +#Preview("Detail Panel Close Button") { + return PreviewSupport.previewHost(theme: .dark, frame: .detail) { + DetailPanelCloseButton(action: {}) + .padding() + } +} +#endif + diff --git a/ChillMac/App/Theme.swift b/ChillMac/App/Theme.swift index e3ca1b7..70a1b53 100644 --- a/ChillMac/App/Theme.swift +++ b/ChillMac/App/Theme.swift @@ -102,3 +102,14 @@ struct ThemedView: View { .preferredColorScheme(settings.preferredColorScheme) } } + +#if DEBUG +#Preview("ThemedView") { + return ThemedView(content: + Text("Sample") + .padding(20) + ) + .frame(width: 200, height: 100) +} +#endif + diff --git a/ChillMac/Fan/SystemInfo.swift b/ChillMac/Fan/SystemInfo.swift index 7d5f1e5..4874b66 100644 --- a/ChillMac/Fan/SystemInfo.swift +++ b/ChillMac/Fan/SystemInfo.swift @@ -59,6 +59,31 @@ final class SystemInfo: ObservableObject { refreshDynamic() } + #if DEBUG + /// Canned sample for previews/tests — skips `fetchHardwareInfo()` and timers. + static var previewSample: SystemInfo { + SystemInfo(previewSample: ()) + } + + private init(previewSample: Void) { + machineModel = "MacBook Pro" + chipName = "Apple M3 Pro" + ramAmount = "36 GB" + macOSVersion = "macOS 15.0.0" + diskUsage = "256 GB" + uptime = "2d 5h 12m" + diskTotalBytes = 512_110_190_592 + diskAvailableBytes = 275_146_342_400 + diskCategories = [ + DiskCategory(name: "Applications", bytes: 45_000_000_000, color: .systemRed), + DiskCategory(name: "Downloads", bytes: 12_000_000_000, color: .systemPink), + DiskCategory(name: "Documents", bytes: 28_000_000_000, color: .systemBlue), + DiskCategory(name: "Desktop", bytes: 4_000_000_000, color: .systemGreen), + DiskCategory(name: "Other", bytes: 148_000_000_000, color: .systemGray), + ] + } + #endif + func startMonitoring() { guard timer == nil else { return } refreshDynamic() diff --git a/ChillMac/Preview/PreviewSupport.swift b/ChillMac/Preview/PreviewSupport.swift new file mode 100644 index 0000000..fcd1b38 --- /dev/null +++ b/ChillMac/Preview/PreviewSupport.swift @@ -0,0 +1,179 @@ +#if DEBUG +import SwiftUI + +/// Shared sample data for SwiftUI `#Preview` and unit tests. +/// Factories construct and seed only — never start monitors, XPC, or network. +enum PreviewSupport { + // MARK: - Sample values + + static let sampleFans: [FanInfo] = [ + FanInfo( + id: 0, + name: "Left Fan", + currentRPM: 2800, + minRPM: 1200, + maxRPM: 6000, + targetRPM: 2800, + isManualMode: false + ), + FanInfo( + id: 1, + name: "Right Fan", + currentRPM: 2650, + minRPM: 1200, + maxRPM: 6000, + targetRPM: 2650, + isManualMode: true + ), + ] + + static let sampleSensors: [TemperatureSensor] = [ + TemperatureSensor(id: "Tp09", label: "CPU Die", temperature: 58.5), + TemperatureSensor(id: "TG0P", label: "GPU", temperature: 52.0), + TemperatureSensor(id: "TH0x", label: "SSD", temperature: 41.2), + TemperatureSensor(id: "TB0T", label: "Battery", temperature: 33.8), + ] + + private static let sampleCpuHistory: [Double] = [ + 12, 18, 22, 35, 48, 42, 38, 55, 62, 58, + 45, 40, 33, 28, 25, 30, 44, 50, 47, 36, + 28, 22, 18, 15, + ] + + // MARK: - Factories + + static var fanMonitor: FanMonitor { + let monitor = FanMonitor() + monitor.helperReady = true + monitor.fans = sampleFans + monitor.sensors = sampleSensors + monitor.peakTemperature = 58.5 + monitor.peakTemperatureLabel = "CPU Die" + monitor.peakCpuTemperature = 58.5 + monitor.peakGpuTemperature = 52.0 + monitor.peakSsdTemperature = 41.2 + return monitor + } + + /// Fan monitor seeded like Performance Mode Max under load (matches expanded Fans card). + static var fanMonitorPerformanceActive: FanMonitor { + let monitor = fanMonitor + monitor.peakTemperature = 92.2 + monitor.peakTemperatureLabel = "CPU Die" + monitor.peakCpuTemperature = 92.2 + monitor.performanceCurvePercent = 100 + return monitor + } + + /// Posts the same notification StatusBarController sends when the popover opens, + /// so `@State appeared` fades content in during canvas previews. + static func triggerPopoverAppeared() { + NotificationCenter.default.post(name: .popoverDidShow, object: nil) + } + + static var cpuInfo: CpuInfo { + let info = CpuInfo() + info.userPercent = 28 + info.systemPercent = 12 + info.idlePercent = 60 + info.totalUsage = 40 + info.history = sampleCpuHistory + info.userHistory = sampleCpuHistory.map { $0 * 0.7 } + info.systemHistory = sampleCpuHistory.map { $0 * 0.3 } + info.topProcesses = [ + .init(name: "Xcode", cpuPercent: 42.5, icon: nil), + .init(name: "Safari", cpuPercent: 18.2, icon: nil), + .init(name: "Finder", cpuPercent: 4.1, icon: nil), + ] + return info + } + + static var memoryInfo: MemoryInfo { + let info = MemoryInfo() + let total = info.totalMemory + info.activeMemory = UInt64(Double(total) * 0.35) + info.wiredMemory = UInt64(Double(total) * 0.15) + info.compressedMemory = UInt64(Double(total) * 0.10) + info.availableMemory = total - info.activeMemory - info.wiredMemory - info.compressedMemory + info.pressurePercent = 60 + info.swapUsed = 512 * 1_048_576 + info.topProcesses = [ + .init(name: "Xcode", memoryBytes: 2_147_483_648, icon: nil), + .init(name: "Safari", memoryBytes: 805_306_368, icon: nil), + .init(name: "Finder", memoryBytes: 268_435_456, icon: nil), + ] + return info + } + + static var batteryInfo: BatteryInfo { + let info = BatteryInfo() + info.currentCharge = 72 + info.maxCapacity = 4800 + info.designCapacity = 5200 + info.cycleCount = 312 + info.healthPercent = 92 + info.temperature = 31.5 + info.isCharging = false + info.isPluggedIn = true + info.timeRemaining = "4h 22m" + info.condition = "Normal" + return info + } + + static var systemInfo: SystemInfo { + SystemInfo.previewSample + } + + static var fpsMonitor: DisplayFPSMonitor { + let monitor = DisplayFPSMonitor() + monitor.fps = 120 + return monitor + } + + static var updateChecker: UpdateChecker { + let checker = UpdateChecker() + checker.hasChecked = true + checker.updateAvailable = false + checker.latestVersion = checker.currentVersion + return checker + } + + static var helper: HelperConnection { + HelperConnection() + } + + // MARK: - Preview chrome + + enum PreviewFrame { + case popover + case detail + } + + static func previewHost( + theme: AppTheme = .dark, + frame: PreviewFrame = .popover, + @ViewBuilder content: () -> Content + ) -> some View { + let size: CGSize = { + switch frame { + case .popover: return CGSize(width: 420, height: 640) + case .detail: return CGSize(width: 360, height: 560) + } + }() + return content() + .environment(\.theme, theme) + .preferredColorScheme(themeIsDark(theme) ? .dark : .light) + .frame(width: size.width, height: size.height) + } + + private static func themeIsDark(_ theme: AppTheme) -> Bool { + theme.bgGradientTop == AppTheme.dark.bgGradientTop + } +} + +extension View { + func previewHost(theme: AppTheme = .dark, frame: PreviewSupport.PreviewFrame = .popover) -> some View { + PreviewSupport.previewHost(theme: theme, frame: frame) { self } + } +} +#endif diff --git a/ChillMac/Views/BatteryDetailView.swift b/ChillMac/Views/BatteryDetailView.swift index 14ec527..929e932 100644 --- a/ChillMac/Views/BatteryDetailView.swift +++ b/ChillMac/Views/BatteryDetailView.swift @@ -262,3 +262,28 @@ private struct BatteryDetailRow: View { .padding(.vertical, 8) } } + +#if DEBUG +#Preview("Battery Detail — Dark") { + BatteryDetailView( + batteryInfo: PreviewSupport.batteryInfo, + settings: AppSettings.shared + ) + .previewHost(theme: .dark, frame: .detail) +} + +#Preview("Battery Detail — Light") { + BatteryDetailView( + batteryInfo: PreviewSupport.batteryInfo, + settings: AppSettings.shared + ) + .previewHost(theme: .light, frame: .detail) +} + +#Preview("Battery Detail Row") { + let info = PreviewSupport.batteryInfo + return BatteryDetailRow(label: "Condition", value: info.condition) + .padding() + .previewHost(theme: .dark, frame: .detail) +} +#endif diff --git a/ChillMac/Views/CpuDetailView.swift b/ChillMac/Views/CpuDetailView.swift index 3aa8bbe..3b9e108 100644 --- a/ChillMac/Views/CpuDetailView.swift +++ b/ChillMac/Views/CpuDetailView.swift @@ -367,3 +367,46 @@ struct CpuGraphView: View { } } } + +#if DEBUG +#Preview("CpuDetailView Dark") { + CpuDetailView( + cpuInfo: PreviewSupport.cpuInfo, + systemInfo: PreviewSupport.systemInfo, + monitor: PreviewSupport.fanMonitor, + settings: AppSettings.shared + ) + .previewHost(theme: .dark, frame: .detail) +} + +#Preview("CpuDetailView Light") { + CpuDetailView( + cpuInfo: PreviewSupport.cpuInfo, + systemInfo: PreviewSupport.systemInfo, + monitor: PreviewSupport.fanMonitor, + settings: AppSettings.shared + ) + .previewHost(theme: .light, frame: .detail) +} + +#Preview("CpuGraphView") { + let cpu = PreviewSupport.cpuInfo + return CpuGraphView( + history: cpu.history, + userHistory: cpu.userHistory, + systemHistory: cpu.systemHistory + ) + .previewHost(theme: .dark, frame: .detail) +} + +#Preview("UsageRow") { + let cpu = PreviewSupport.cpuInfo + return UsageRow( + color: .teal, + value: String(format: "%.1f %%", 100 - cpu.totalUsage), + label: "Available" + ) + .padding() + .previewHost(theme: .dark, frame: .detail) +} +#endif diff --git a/ChillMac/Views/DiskDetailView.swift b/ChillMac/Views/DiskDetailView.swift index 8170485..922192e 100644 --- a/ChillMac/Views/DiskDetailView.swift +++ b/ChillMac/Views/DiskDetailView.swift @@ -317,3 +317,34 @@ private struct DiskLegendRow: View { } } } + +#if DEBUG +#Preview("Disk Detail — Dark") { + DiskDetailView( + systemInfo: PreviewSupport.systemInfo, + monitor: PreviewSupport.fanMonitor, + settings: AppSettings.shared + ) + .previewHost(theme: .dark, frame: .detail) +} + +#Preview("Disk Detail — Light") { + DiskDetailView( + systemInfo: PreviewSupport.systemInfo, + monitor: PreviewSupport.fanMonitor, + settings: AppSettings.shared + ) + .previewHost(theme: .light, frame: .detail) +} + +#Preview("Disk Legend Row") { + let cat = PreviewSupport.systemInfo.diskCategories[0] + return DiskLegendRow( + color: Color(nsColor: cat.color), + label: cat.name, + value: SystemInfo.formatDiskBytes(cat.bytes) + ) + .padding() + .previewHost(theme: .dark, frame: .detail) +} +#endif diff --git a/ChillMac/Views/FanRowView.swift b/ChillMac/Views/FanRowView.swift index 80ee725..8e000d5 100644 --- a/ChillMac/Views/FanRowView.swift +++ b/ChillMac/Views/FanRowView.swift @@ -188,3 +188,27 @@ struct FanRowView: View { } } } + +#if DEBUG +#Preview("FanRowView Auto") { + let monitor = PreviewSupport.fanMonitor + return FanRowView( + fan: PreviewSupport.sampleFans[0], + helper: PreviewSupport.helper, + monitor: monitor + ) + .previewHost(theme: .dark) +} + +#Preview("FanRowView Manual") { + let monitor = PreviewSupport.fanMonitor + monitor.manualOverrides[1] = true + monitor.targetOverrides[1] = PreviewSupport.sampleFans[1].targetRPM + return FanRowView( + fan: PreviewSupport.sampleFans[1], + helper: PreviewSupport.helper, + monitor: monitor + ) + .previewHost(theme: .dark) +} +#endif diff --git a/ChillMac/Views/MemoryDetailView.swift b/ChillMac/Views/MemoryDetailView.swift index b73385c..09d770e 100644 --- a/ChillMac/Views/MemoryDetailView.swift +++ b/ChillMac/Views/MemoryDetailView.swift @@ -262,3 +262,26 @@ private struct LegendRow: View { } } } + +#if DEBUG +#Preview("Memory Detail — Dark") { + MemoryDetailView(memoryInfo: PreviewSupport.memoryInfo) + .previewHost(theme: .dark, frame: .detail) +} + +#Preview("Memory Detail — Light") { + MemoryDetailView(memoryInfo: PreviewSupport.memoryInfo) + .previewHost(theme: .light, frame: .detail) +} + +#Preview("Legend Row") { + let info = PreviewSupport.memoryInfo + return LegendRow( + color: .green, + label: "Active", + value: MemoryInfo.formatBytes(info.activeMemory) + ) + .padding() + .previewHost(theme: .dark, frame: .detail) +} +#endif diff --git a/ChillMac/Views/PanelResizeHandle.swift b/ChillMac/Views/PanelResizeHandle.swift index b109cb0..36b79c8 100644 --- a/ChillMac/Views/PanelResizeHandle.swift +++ b/ChillMac/Views/PanelResizeHandle.swift @@ -61,3 +61,15 @@ struct PanelResizeHandle: View { } } } + +#if DEBUG +#Preview("Panel Resize Handle") { + return PreviewSupport.previewHost(theme: .dark, frame: .detail) { + VStack { + Spacer() + PanelResizeHandle(panelHeight: .constant(560)) + } + } +} +#endif + diff --git a/ChillMac/Views/PopoverView.swift b/ChillMac/Views/PopoverView.swift index 1566bdb..15e3085 100644 --- a/ChillMac/Views/PopoverView.swift +++ b/ChillMac/Views/PopoverView.swift @@ -580,3 +580,65 @@ struct SectionHeader: View { CardSectionHeader(title: title) } } + +#if DEBUG +#Preview("PopoverView Dark") { + AppSettings.shared.appearanceMode = .dark + AppSettings.shared.performanceMode = true + AppSettings.shared.performanceLevel = .max + return PopoverView( + monitor: PreviewSupport.fanMonitorPerformanceActive, + settings: AppSettings.shared, + systemInfo: PreviewSupport.systemInfo, + batteryInfo: PreviewSupport.batteryInfo, + cpuInfo: PreviewSupport.cpuInfo, + memoryInfo: PreviewSupport.memoryInfo, + fpsMonitor: PreviewSupport.fpsMonitor, + updateChecker: PreviewSupport.updateChecker, + helper: PreviewSupport.helper + ) + .previewHost(theme: .dark) + .onAppear { PreviewSupport.triggerPopoverAppeared() } +} + +#Preview("PopoverView Light") { + AppSettings.shared.appearanceMode = .light + AppSettings.shared.performanceMode = true + AppSettings.shared.performanceLevel = .max + return PopoverView( + monitor: PreviewSupport.fanMonitorPerformanceActive, + settings: AppSettings.shared, + systemInfo: PreviewSupport.systemInfo, + batteryInfo: PreviewSupport.batteryInfo, + cpuInfo: PreviewSupport.cpuInfo, + memoryInfo: PreviewSupport.memoryInfo, + fpsMonitor: PreviewSupport.fpsMonitor, + updateChecker: PreviewSupport.updateChecker, + helper: PreviewSupport.helper + ) + .previewHost(theme: .light) + .onAppear { PreviewSupport.triggerPopoverAppeared() } +} + +#Preview("CardSectionHeader") { + CardSectionHeader(title: "Fans") + .previewHost(theme: .dark) +} + +#Preview("InfoCard") { + let info = PreviewSupport.systemInfo + return InfoCard( + icon: "cpu", + title: info.chipName, + subtitle: "Processor", + accent: .teal, + onTap: {} + ) + .previewHost(theme: .dark) +} + +#Preview("SectionHeader") { + SectionHeader(title: "Temperatures") + .previewHost(theme: .dark) +} +#endif diff --git a/ChillMac/Views/SettingsView.swift b/ChillMac/Views/SettingsView.swift index ab1747a..732685e 100644 --- a/ChillMac/Views/SettingsView.swift +++ b/ChillMac/Views/SettingsView.swift @@ -586,3 +586,36 @@ struct SettingsView: View { } } } + +#if DEBUG +#Preview("Settings Dark") { + return PreviewSupport.previewHost(theme: .dark, frame: .popover) { + SettingsView( + settings: AppSettings.shared, + updateChecker: PreviewSupport.updateChecker, + systemInfo: PreviewSupport.systemInfo, + fanMonitor: PreviewSupport.fanMonitor, + cpuInfo: PreviewSupport.cpuInfo, + memoryInfo: PreviewSupport.memoryInfo, + batteryInfo: PreviewSupport.batteryInfo, + onDismiss: {} + ) + } +} + +#Preview("Settings Light") { + return PreviewSupport.previewHost(theme: .light, frame: .popover) { + SettingsView( + settings: AppSettings.shared, + updateChecker: PreviewSupport.updateChecker, + systemInfo: PreviewSupport.systemInfo, + fanMonitor: PreviewSupport.fanMonitor, + cpuInfo: PreviewSupport.cpuInfo, + memoryInfo: PreviewSupport.memoryInfo, + batteryInfo: PreviewSupport.batteryInfo, + onDismiss: {} + ) + } +} +#endif + diff --git a/ChillMac/Views/TemperatureDetailView.swift b/ChillMac/Views/TemperatureDetailView.swift index 15259df..1b72a71 100644 --- a/ChillMac/Views/TemperatureDetailView.swift +++ b/ChillMac/Views/TemperatureDetailView.swift @@ -228,3 +228,21 @@ struct TemperatureDetailView: View { } } } + +#if DEBUG +#Preview("TemperatureDetailView Dark") { + TemperatureDetailView( + monitor: PreviewSupport.fanMonitor, + settings: AppSettings.shared + ) + .previewHost(theme: .dark, frame: .detail) +} + +#Preview("TemperatureDetailView Light") { + TemperatureDetailView( + monitor: PreviewSupport.fanMonitor, + settings: AppSettings.shared + ) + .previewHost(theme: .light, frame: .detail) +} +#endif diff --git a/ChillMac/Views/TemperatureRowView.swift b/ChillMac/Views/TemperatureRowView.swift index ae74ce8..62aaff3 100644 --- a/ChillMac/Views/TemperatureRowView.swift +++ b/ChillMac/Views/TemperatureRowView.swift @@ -39,3 +39,18 @@ struct TemperatureRowView: View { } } } + +#if DEBUG +#Preview("TemperatureRowView") { + VStack(spacing: 8) { + ForEach(PreviewSupport.sampleSensors) { sensor in + TemperatureRowView( + sensor: sensor, + settings: AppSettings.shared + ) + } + } + .padding() + .previewHost(theme: .dark) +} +#endif diff --git a/ChillMacTests/Fixtures/.gitkeep b/ChillMacTests/Fixtures/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ChillMacTests/Fixtures/PreviewSupportTests.swift b/ChillMacTests/Fixtures/PreviewSupportTests.swift new file mode 100644 index 0000000..03ecacb --- /dev/null +++ b/ChillMacTests/Fixtures/PreviewSupportTests.swift @@ -0,0 +1,33 @@ +import Testing +@testable import ChillMac + +@Suite("PreviewSupport", .serialized) +struct PreviewSupportTests { + @Test("fanMonitor seeds fans and never needs helper for display state") + func fanMonitorSeeded() { + let monitor = PreviewSupport.fanMonitor + #expect(monitor.fans.count >= 1) + #expect(monitor.helperReady == true) + #expect(monitor.sensors.count >= 1) + // Implicit contract: caller must not call startMonitoring in fixtures + } + + @Test("systemInfo preview sample skips live profiler placeholders") + func systemInfoPreview() { + let info = SystemInfo.previewSample + #expect(!info.chipName.contains("...")) + #expect(info.diskTotalBytes > 0) + } + + @Test("all screen fixtures construct without starting monitors") + func screenFixturesConstruct() { + _ = PreviewSupport.fanMonitor + _ = PreviewSupport.cpuInfo + _ = PreviewSupport.memoryInfo + _ = PreviewSupport.batteryInfo + _ = PreviewSupport.systemInfo + _ = PreviewSupport.fpsMonitor + _ = PreviewSupport.updateChecker + _ = PreviewSupport.helper + } +} diff --git a/ChillMacTests/Support/.gitkeep b/ChillMacTests/Support/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ChillMacTests/Support/HarnessSmokeTests.swift b/ChillMacTests/Support/HarnessSmokeTests.swift new file mode 100644 index 0000000..ef1810e --- /dev/null +++ b/ChillMacTests/Support/HarnessSmokeTests.swift @@ -0,0 +1,5 @@ +import Testing + +@Test func harnessSmoke() { + #expect(true) +} diff --git a/TestPlans/Unit.xctestplan b/TestPlans/Unit.xctestplan new file mode 100644 index 0000000..82075ba --- /dev/null +++ b/TestPlans/Unit.xctestplan @@ -0,0 +1,29 @@ +{ + "configurations" : [ + { + "id" : "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:ChillMac.xcodeproj", + "identifier" : "ChillMac", + "name" : "ChillMac" + } + }, + "testTargets" : [ + { + "parallelizable" : true, + "target" : { + "containerPath" : "container:ChillMac.xcodeproj", + "identifier" : "ChillMacTests", + "name" : "ChillMacTests" + } + } + ], + "version" : 1 +} diff --git a/project.yml b/project.yml index 6bc1beb..4bd4c0a 100644 --- a/project.yml +++ b/project.yml @@ -39,6 +39,21 @@ targets: outputFiles: - "$(BUILT_PRODUCTS_DIR)/$(CONTENTS_FOLDER_PATH)/Library/LaunchServices/com.idevtim.ChillMac.Helper" + ChillMacTests: + type: bundle.unit-test + platform: macOS + sources: + - path: ChillMacTests + dependencies: + - target: ChillMac + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.idevtim.ChillMacTests + GENERATE_INFOPLIST_FILE: YES + CODE_SIGN_IDENTITY: "-" + CODE_SIGN_STYLE: Automatic + LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks" + com.idevtim.ChillMac.Helper: type: tool platform: macOS @@ -57,3 +72,17 @@ targets: CREATE_INFOPLIST_SECTION_IN_BINARY: YES INFOPLIST_OTHER_PREPROCESSOR_FLAGS: "" OTHER_LDFLAGS: "-sectcreate __TEXT __launchd_plist $(SRCROOT)/FanControlHelper/Launchd.plist" + +schemes: + ChillMac: + build: + targets: + ChillMac: all + ChillMacTests: [test] + test: + targets: + - name: ChillMacTests + parallelizable: true + testPlans: + - path: TestPlans/Unit.xctestplan + defaultPlan: true