diff --git a/ClaudeMeter.xcodeproj/xcshareddata/xcschemes/ClaudeMeter.xcscheme b/ClaudeMeter.xcodeproj/xcshareddata/xcschemes/ClaudeMeter.xcscheme index b90c50c..c27f3be 100644 --- a/ClaudeMeter.xcodeproj/xcshareddata/xcschemes/ClaudeMeter.xcscheme +++ b/ClaudeMeter.xcodeproj/xcshareddata/xcschemes/ClaudeMeter.xcscheme @@ -27,8 +27,18 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" + shouldUseLaunchSchemeArgsEnv = "NO" shouldAutocreateTestPlan = "YES"> + + + + + + = .image( + precision: 0.99, + perceptualPrecision: 0.90 + ) + func test_menuBarIcon_showsBatteryStyleWhenWarning() { let image = renderIcon(style: .battery) - assertSnapshot(of: image, as: .image, record: isRecording) + assertSnapshot(of: image, as: strategy, record: isRecording) } func test_menuBarIcon_showsCircularStyleWhenWarning() { let image = renderIcon(style: .circular) - assertSnapshot(of: image, as: .image, record: isRecording) + assertSnapshot(of: image, as: strategy, record: isRecording) } func test_menuBarIcon_showsMinimalStyleWhenWarning() { let image = renderIcon(style: .minimal) - assertSnapshot(of: image, as: .image, record: isRecording) + assertSnapshot(of: image, as: strategy, record: isRecording) } func test_menuBarIcon_showsSegmentsStyleWhenWarning() { let image = renderIcon(style: .segments) - assertSnapshot(of: image, as: .image, record: isRecording) + assertSnapshot(of: image, as: strategy, record: isRecording) } func test_menuBarIcon_showsDualBarStyleWhenWarning() { let image = renderIcon(style: .dualBar) - assertSnapshot(of: image, as: .image, record: isRecording) + assertSnapshot(of: image, as: strategy, record: isRecording) } func test_menuBarIcon_showsGaugeStyleWhenWarning() { let image = renderIcon(style: .gauge) - assertSnapshot(of: image, as: .image, record: isRecording) + assertSnapshot(of: image, as: strategy, record: isRecording) } func test_menuBarIcon_showsLoadingIndicatorInBatteryStyle() { let image = renderIcon(style: .battery, status: .safe, isLoading: true) - assertSnapshot(of: image, as: .image, record: isRecording) + assertSnapshot(of: image, as: strategy, record: isRecording) } func test_menuBarIcon_showsStaleIndicatorInBatteryStyle() { let image = renderIcon(style: .battery, status: .safe, isStale: true) - assertSnapshot(of: image, as: .image, record: isRecording) + assertSnapshot(of: image, as: strategy, record: isRecording) + } + + /// Keeps the tolerance above honest: it must still reject an icon whose status colour + /// changed, otherwise the snapshots assert nothing. + func test_snapshotTolerance_rejectsChangedStatusColour() { + let warning = renderIcon(style: .battery, status: .warning) + let critical = renderIcon(style: .battery, status: .critical) + + XCTAssertNotNil( + strategy.diffing.diff(warning, critical), + "Tolerance is too loose: a changed status colour compares as unchanged" + ) } private func renderIcon( diff --git a/ClaudeMeterTests/TestHostLaunchTests.swift b/ClaudeMeterTests/TestHostLaunchTests.swift new file mode 100644 index 0000000..06a5e8f --- /dev/null +++ b/ClaudeMeterTests/TestHostLaunchTests.swift @@ -0,0 +1,38 @@ +// +// TestHostLaunchTests.swift +// ClaudeMeterTests +// + +import XCTest +@testable import ClaudeMeter + +/// The test bundle is hosted by the app, so running any test launches the real app. +/// Left alone it would bootstrap the menu bar, read the session key from the Keychain +/// (prompting on every run, since the test binary is signed differently from the +/// installed app) and fetch live usage over the network. +/// +/// The scheme's test action passes `--demo safeUsage`, which routes the app to +/// `startWithoutBootstrap()`. That keeps the workaround out of the production path, +/// at the cost of living in the scheme where it is easy to delete by accident — +/// hence this test. +final class TestHostLaunchTests: XCTestCase { + func test_testHost_launchesWithoutBootstrappingTheMenuBar() throws { + let arguments = ProcessInfo.processInfo.arguments + + guard let flag = arguments.firstIndex(of: "--demo") else { + return XCTFail( + """ + The test host was launched without `--demo`, so it will bootstrap the menu bar: \ + read the Keychain and hit the network during tests. Restore the CommandLineArguments \ + in the scheme's TestAction (ClaudeMeter.xcscheme). + """ + ) + } + + let mode = arguments[arguments.index(after: flag)] + XCTAssertNotNil( + DemoMode(rawValue: mode), + "`--demo \(mode)` is not a DemoMode, so the app will bootstrap normally." + ) + } +}