pi-remote-ios/Tests/CoreTests/ModifierStateTests.swift

114 lines
3.3 KiB
Swift

// ModifierStateTests.swift
// Unit tests for ModifierState the sticky-modifier observable for ModifierBar.
//
// ModifierState is pure state (no UIKit, no networking) so all tests are
// synchronous. The class is @MainActor; every test method is also marked
// @MainActor to satisfy Swift 6 strict concurrency.
import XCTest
@testable import piRemote
@MainActor
final class ModifierStateTests: XCTestCase {
// Fresh instance per test.
private var state: ModifierState!
override func setUp() async throws {
state = ModifierState()
}
override func tearDown() async throws {
state = nil
}
// MARK: - Initial state
/// Both published properties must be `false` immediately after init.
func testInitialState_bothFalse() {
XCTAssertFalse(state.ctrlActive, "ctrlActive should start as false")
XCTAssertFalse(state.isRepeating, "isRepeating should start as false")
}
// MARK: - toggleCtrl
/// First toggle arms the Ctrl modifier.
func testToggleCtrl_armsModifier() {
state.toggleCtrl()
XCTAssertTrue(state.ctrlActive)
}
/// Second toggle disarms it.
func testToggleCtrl_disarmsModifier() {
state.toggleCtrl() // arm
state.toggleCtrl() // disarm
XCTAssertFalse(state.ctrlActive)
}
/// Six successive toggles cycle true/false/true/false/true/false correctly.
func testMultipleToggles_cycleCorrectly() {
for i in 1...6 {
state.toggleCtrl()
let expected = (i % 2 == 1)
XCTAssertEqual(
state.ctrlActive, expected,
"After \(i) toggle(s) ctrlActive should be \(expected)"
)
}
}
/// `toggleCtrl` must not affect `isRepeating`.
func testToggleCtrl_doesNotAffectIsRepeating() {
state.isRepeating = true
state.toggleCtrl()
XCTAssertTrue(state.isRepeating, "toggleCtrl must not touch isRepeating")
}
// MARK: - reset
/// `reset()` clears `ctrlActive` when it was armed.
func testReset_clearsCtrActive() {
state.toggleCtrl()
XCTAssertTrue(state.ctrlActive) // precondition
state.reset()
XCTAssertFalse(state.ctrlActive)
}
/// `reset()` clears `isRepeating` when it was set.
func testReset_clearsIsRepeating() {
state.isRepeating = true
state.reset()
XCTAssertFalse(state.isRepeating)
}
/// `reset()` clears both properties simultaneously.
func testReset_clearsAllState() {
state.toggleCtrl()
state.isRepeating = true
state.reset()
XCTAssertFalse(state.ctrlActive, "ctrlActive must be false after reset()")
XCTAssertFalse(state.isRepeating, "isRepeating must be false after reset()")
}
/// Calling `reset()` on an already-neutral state must not crash or change values.
func testReset_isIdempotent() {
// state is already all-false from setUp
state.reset()
state.reset()
XCTAssertFalse(state.ctrlActive)
XCTAssertFalse(state.isRepeating)
}
/// `reset()` after many toggles returns to neutral.
func testReset_afterManyToggles_returnsToNeutral() {
for _ in 0..<10 { state.toggleCtrl() }
state.isRepeating = true
state.reset()
XCTAssertFalse(state.ctrlActive)
XCTAssertFalse(state.isRepeating)
}
}