Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,18 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldUseLaunchSchemeArgsEnv = "NO"
shouldAutocreateTestPlan = "YES">
<CommandLineArguments>
<CommandLineArgument
argument = "--demo"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "safeUsage"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
<Testables>
<TestableReference
skipped = "NO"
Expand Down
40 changes: 32 additions & 8 deletions ClaudeMeterTests/MenuBarIconSnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,76 @@ import XCTest

@MainActor
final class MenuBarIconSnapshotTests: XCTestCase {
/// SwiftUI renders gradients and antialiased edges differently across macOS versions,
/// so a pixel-exact comparison fails on any OS other than the one that recorded the
/// references. Compare perceptually instead.
///
/// The tolerance is sized from measurement, not taste: the same icon rendered on a
/// newer macOS drifts by at most ~10 ΔE, while a genuinely different icon differs by
/// ~128 ΔE. Allowing ΔE ≤ 10 absorbs the drift and still leaves a 12x margin.
private let strategy: Snapshotting<NSImage, NSImage> = .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(
Expand Down
38 changes: 38 additions & 0 deletions ClaudeMeterTests/TestHostLaunchTests.swift
Original file line number Diff line number Diff line change
@@ -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."
)
}
}