feat: add lottie for animations

This commit is contained in:
Michael Freno
2026-01-08 23:01:02 -05:00
parent 587b300a3c
commit a14b7e7b99
18 changed files with 1708 additions and 67 deletions

View File

@@ -0,0 +1,53 @@
//
// AnimationAssetTests.swift
// GazeTests
//
// Created by Mike Freno on 1/8/26.
//
import XCTest
@testable import Gaze
final class AnimationAssetTests: XCTestCase {
func testRawValues() {
XCTAssertEqual(AnimationAsset.blink.rawValue, "blink")
XCTAssertEqual(AnimationAsset.lookAway.rawValue, "look-away")
XCTAssertEqual(AnimationAsset.posture.rawValue, "posture")
}
func testFileNames() {
XCTAssertEqual(AnimationAsset.blink.fileName, "blink")
XCTAssertEqual(AnimationAsset.lookAway.fileName, "look-away")
XCTAssertEqual(AnimationAsset.posture.fileName, "posture")
}
func testFileNameMatchesRawValue() {
XCTAssertEqual(AnimationAsset.blink.fileName, AnimationAsset.blink.rawValue)
XCTAssertEqual(AnimationAsset.lookAway.fileName, AnimationAsset.lookAway.rawValue)
XCTAssertEqual(AnimationAsset.posture.fileName, AnimationAsset.posture.rawValue)
}
func testInitFromRawValue() {
XCTAssertEqual(AnimationAsset(rawValue: "blink"), .blink)
XCTAssertEqual(AnimationAsset(rawValue: "look-away"), .lookAway)
XCTAssertEqual(AnimationAsset(rawValue: "posture"), .posture)
XCTAssertNil(AnimationAsset(rawValue: "invalid"))
}
func testEquality() {
XCTAssertEqual(AnimationAsset.blink, AnimationAsset.blink)
XCTAssertNotEqual(AnimationAsset.blink, AnimationAsset.lookAway)
XCTAssertNotEqual(AnimationAsset.lookAway, AnimationAsset.posture)
}
func testAllCasesExist() {
let blink = AnimationAsset.blink
let lookAway = AnimationAsset.lookAway
let posture = AnimationAsset.posture
XCTAssertNotNil(blink)
XCTAssertNotNil(lookAway)
XCTAssertNotNil(posture)
}
}

View File

@@ -0,0 +1,175 @@
//
// AppSettingsTests.swift
// GazeTests
//
// Created by Mike Freno on 1/8/26.
//
import XCTest
@testable import Gaze
final class AppSettingsTests: XCTestCase {
func testDefaultSettings() {
let settings = AppSettings.defaults
XCTAssertTrue(settings.lookAwayTimer.enabled)
XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, 20 * 60)
XCTAssertEqual(settings.lookAwayCountdownSeconds, 20)
XCTAssertTrue(settings.blinkTimer.enabled)
XCTAssertEqual(settings.blinkTimer.intervalSeconds, 5 * 60)
XCTAssertTrue(settings.postureTimer.enabled)
XCTAssertEqual(settings.postureTimer.intervalSeconds, 30 * 60)
XCTAssertFalse(settings.hasCompletedOnboarding)
XCTAssertFalse(settings.launchAtLogin)
XCTAssertTrue(settings.playSounds)
}
func testEquality() {
let settings1 = AppSettings.defaults
let settings2 = AppSettings.defaults
XCTAssertEqual(settings1, settings2)
}
func testInequalityWhenLookAwayTimerDiffers() {
var settings1 = AppSettings.defaults
var settings2 = AppSettings.defaults
settings2.lookAwayTimer.enabled = false
XCTAssertNotEqual(settings1, settings2)
settings2.lookAwayTimer.enabled = true
settings2.lookAwayTimer.intervalSeconds = 10 * 60
XCTAssertNotEqual(settings1, settings2)
}
func testInequalityWhenCountdownDiffers() {
var settings1 = AppSettings.defaults
var settings2 = AppSettings.defaults
settings2.lookAwayCountdownSeconds = 30
XCTAssertNotEqual(settings1, settings2)
}
func testInequalityWhenBlinkTimerDiffers() {
var settings1 = AppSettings.defaults
var settings2 = AppSettings.defaults
settings2.blinkTimer.enabled = false
XCTAssertNotEqual(settings1, settings2)
}
func testInequalityWhenPostureTimerDiffers() {
var settings1 = AppSettings.defaults
var settings2 = AppSettings.defaults
settings2.postureTimer.intervalSeconds = 60 * 60
XCTAssertNotEqual(settings1, settings2)
}
func testInequalityWhenOnboardingDiffers() {
var settings1 = AppSettings.defaults
var settings2 = AppSettings.defaults
settings2.hasCompletedOnboarding = true
XCTAssertNotEqual(settings1, settings2)
}
func testInequalityWhenLaunchAtLoginDiffers() {
var settings1 = AppSettings.defaults
var settings2 = AppSettings.defaults
settings2.launchAtLogin = true
XCTAssertNotEqual(settings1, settings2)
}
func testInequalityWhenPlaySoundsDiffers() {
var settings1 = AppSettings.defaults
var settings2 = AppSettings.defaults
settings2.playSounds = false
XCTAssertNotEqual(settings1, settings2)
}
func testCodableEncoding() throws {
let settings = AppSettings.defaults
let encoder = JSONEncoder()
let data = try encoder.encode(settings)
XCTAssertNotNil(data)
XCTAssertGreaterThan(data.count, 0)
}
func testCodableDecoding() throws {
let settings = AppSettings.defaults
let encoder = JSONEncoder()
let data = try encoder.encode(settings)
let decoder = JSONDecoder()
let decoded = try decoder.decode(AppSettings.self, from: data)
XCTAssertEqual(decoded, settings)
}
func testCodableRoundTripWithModifiedSettings() throws {
var settings = AppSettings.defaults
settings.lookAwayTimer.enabled = false
settings.lookAwayCountdownSeconds = 30
settings.blinkTimer.intervalSeconds = 10 * 60
settings.postureTimer.enabled = false
settings.hasCompletedOnboarding = true
settings.launchAtLogin = true
settings.playSounds = false
let encoder = JSONEncoder()
let data = try encoder.encode(settings)
let decoder = JSONDecoder()
let decoded = try decoder.decode(AppSettings.self, from: data)
XCTAssertEqual(decoded, settings)
XCTAssertFalse(decoded.lookAwayTimer.enabled)
XCTAssertEqual(decoded.lookAwayCountdownSeconds, 30)
XCTAssertEqual(decoded.blinkTimer.intervalSeconds, 10 * 60)
XCTAssertFalse(decoded.postureTimer.enabled)
XCTAssertTrue(decoded.hasCompletedOnboarding)
XCTAssertTrue(decoded.launchAtLogin)
XCTAssertFalse(decoded.playSounds)
}
func testMutability() {
var settings = AppSettings.defaults
settings.lookAwayTimer.enabled = false
XCTAssertFalse(settings.lookAwayTimer.enabled)
settings.lookAwayCountdownSeconds = 30
XCTAssertEqual(settings.lookAwayCountdownSeconds, 30)
settings.hasCompletedOnboarding = true
XCTAssertTrue(settings.hasCompletedOnboarding)
settings.launchAtLogin = true
XCTAssertTrue(settings.launchAtLogin)
settings.playSounds = false
XCTAssertFalse(settings.playSounds)
}
func testBoundaryValues() {
var settings = AppSettings.defaults
settings.lookAwayTimer.intervalSeconds = 0
XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, 0)
settings.lookAwayCountdownSeconds = 0
XCTAssertEqual(settings.lookAwayCountdownSeconds, 0)
settings.lookAwayTimer.intervalSeconds = Int.max
XCTAssertEqual(settings.lookAwayTimer.intervalSeconds, Int.max)
}
}

View File

@@ -0,0 +1,115 @@
//
// ReminderEventTests.swift
// GazeTests
//
// Created by Mike Freno on 1/8/26.
//
import XCTest
@testable import Gaze
final class ReminderEventTests: XCTestCase {
func testLookAwayTriggeredCreation() {
let event = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
if case .lookAwayTriggered(let countdown) = event {
XCTAssertEqual(countdown, 20)
} else {
XCTFail("Expected lookAwayTriggered event")
}
}
func testBlinkTriggeredCreation() {
let event = ReminderEvent.blinkTriggered
if case .blinkTriggered = event {
XCTAssertTrue(true)
} else {
XCTFail("Expected blinkTriggered event")
}
}
func testPostureTriggeredCreation() {
let event = ReminderEvent.postureTriggered
if case .postureTriggered = event {
XCTAssertTrue(true)
} else {
XCTFail("Expected postureTriggered event")
}
}
func testTypePropertyForLookAway() {
let event = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
XCTAssertEqual(event.type, .lookAway)
}
func testTypePropertyForBlink() {
let event = ReminderEvent.blinkTriggered
XCTAssertEqual(event.type, .blink)
}
func testTypePropertyForPosture() {
let event = ReminderEvent.postureTriggered
XCTAssertEqual(event.type, .posture)
}
func testEquality() {
let event1 = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
let event2 = ReminderEvent.lookAwayTriggered(countdownSeconds: 20)
let event3 = ReminderEvent.lookAwayTriggered(countdownSeconds: 30)
let event4 = ReminderEvent.blinkTriggered
let event5 = ReminderEvent.blinkTriggered
let event6 = ReminderEvent.postureTriggered
XCTAssertEqual(event1, event2)
XCTAssertNotEqual(event1, event3)
XCTAssertNotEqual(event1, event4)
XCTAssertEqual(event4, event5)
XCTAssertNotEqual(event4, event6)
}
func testDifferentCountdownValues() {
let event1 = ReminderEvent.lookAwayTriggered(countdownSeconds: 0)
let event2 = ReminderEvent.lookAwayTriggered(countdownSeconds: 10)
let event3 = ReminderEvent.lookAwayTriggered(countdownSeconds: 60)
XCTAssertNotEqual(event1, event2)
XCTAssertNotEqual(event2, event3)
XCTAssertNotEqual(event1, event3)
XCTAssertEqual(event1.type, .lookAway)
XCTAssertEqual(event2.type, .lookAway)
XCTAssertEqual(event3.type, .lookAway)
}
func testNegativeCountdown() {
let event = ReminderEvent.lookAwayTriggered(countdownSeconds: -5)
if case .lookAwayTriggered(let countdown) = event {
XCTAssertEqual(countdown, -5)
} else {
XCTFail("Expected lookAwayTriggered event")
}
}
func testSwitchExhaustivenessWithAllCases() {
let events: [ReminderEvent] = [
.lookAwayTriggered(countdownSeconds: 20),
.blinkTriggered,
.postureTriggered
]
for event in events {
switch event {
case .lookAwayTriggered:
XCTAssertEqual(event.type, .lookAway)
case .blinkTriggered:
XCTAssertEqual(event.type, .blink)
case .postureTriggered:
XCTAssertEqual(event.type, .posture)
}
}
}
}

View File

@@ -0,0 +1,124 @@
//
// TimerConfigurationTests.swift
// GazeTests
//
// Created by Mike Freno on 1/8/26.
//
import XCTest
@testable import Gaze
final class TimerConfigurationTests: XCTestCase {
func testInitialization() {
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
XCTAssertTrue(config.enabled)
XCTAssertEqual(config.intervalSeconds, 1200)
}
func testInitializationDisabled() {
let config = TimerConfiguration(enabled: false, intervalSeconds: 600)
XCTAssertFalse(config.enabled)
XCTAssertEqual(config.intervalSeconds, 600)
}
func testIntervalMinutesGetter() {
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
XCTAssertEqual(config.intervalMinutes, 20)
}
func testIntervalMinutesSetter() {
var config = TimerConfiguration(enabled: true, intervalSeconds: 0)
config.intervalMinutes = 15
XCTAssertEqual(config.intervalMinutes, 15)
XCTAssertEqual(config.intervalSeconds, 900)
}
func testIntervalMinutesConversion() {
var config = TimerConfiguration(enabled: true, intervalSeconds: 0)
config.intervalMinutes = 1
XCTAssertEqual(config.intervalSeconds, 60)
config.intervalMinutes = 60
XCTAssertEqual(config.intervalSeconds, 3600)
config.intervalMinutes = 0
XCTAssertEqual(config.intervalSeconds, 0)
}
func testEquality() {
let config1 = TimerConfiguration(enabled: true, intervalSeconds: 1200)
let config2 = TimerConfiguration(enabled: true, intervalSeconds: 1200)
let config3 = TimerConfiguration(enabled: false, intervalSeconds: 1200)
let config4 = TimerConfiguration(enabled: true, intervalSeconds: 600)
XCTAssertEqual(config1, config2)
XCTAssertNotEqual(config1, config3)
XCTAssertNotEqual(config1, config4)
}
func testCodableEncoding() throws {
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
let encoder = JSONEncoder()
let data = try encoder.encode(config)
XCTAssertNotNil(data)
XCTAssertGreaterThan(data.count, 0)
}
func testCodableDecoding() throws {
let config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
let encoder = JSONEncoder()
let data = try encoder.encode(config)
let decoder = JSONDecoder()
let decoded = try decoder.decode(TimerConfiguration.self, from: data)
XCTAssertEqual(decoded, config)
}
func testCodableRoundTrip() throws {
let configs = [
TimerConfiguration(enabled: true, intervalSeconds: 300),
TimerConfiguration(enabled: false, intervalSeconds: 1200),
TimerConfiguration(enabled: true, intervalSeconds: 1800),
TimerConfiguration(enabled: false, intervalSeconds: 0)
]
let encoder = JSONEncoder()
let decoder = JSONDecoder()
for config in configs {
let data = try encoder.encode(config)
let decoded = try decoder.decode(TimerConfiguration.self, from: data)
XCTAssertEqual(decoded, config)
}
}
func testMutability() {
var config = TimerConfiguration(enabled: true, intervalSeconds: 1200)
config.enabled = false
XCTAssertFalse(config.enabled)
config.intervalSeconds = 600
XCTAssertEqual(config.intervalSeconds, 600)
XCTAssertEqual(config.intervalMinutes, 10)
}
func testZeroInterval() {
let config = TimerConfiguration(enabled: true, intervalSeconds: 0)
XCTAssertEqual(config.intervalSeconds, 0)
XCTAssertEqual(config.intervalMinutes, 0)
}
func testLargeInterval() {
let config = TimerConfiguration(enabled: true, intervalSeconds: 86400)
XCTAssertEqual(config.intervalSeconds, 86400)
XCTAssertEqual(config.intervalMinutes, 1440)
}
}

View File

@@ -0,0 +1,93 @@
//
// TimerStateTests.swift
// GazeTests
//
// Created by Mike Freno on 1/8/26.
//
import XCTest
@testable import Gaze
final class TimerStateTests: XCTestCase {
func testInitialization() {
let state = TimerState(type: .lookAway, intervalSeconds: 1200)
XCTAssertEqual(state.type, .lookAway)
XCTAssertEqual(state.remainingSeconds, 1200)
XCTAssertFalse(state.isPaused)
XCTAssertTrue(state.isActive)
}
func testInitializationWithPausedState() {
let state = TimerState(type: .blink, intervalSeconds: 300, isPaused: true)
XCTAssertEqual(state.type, .blink)
XCTAssertEqual(state.remainingSeconds, 300)
XCTAssertTrue(state.isPaused)
XCTAssertTrue(state.isActive)
}
func testInitializationWithInactiveState() {
let state = TimerState(type: .posture, intervalSeconds: 1800, isPaused: false, isActive: false)
XCTAssertEqual(state.type, .posture)
XCTAssertEqual(state.remainingSeconds, 1800)
XCTAssertFalse(state.isPaused)
XCTAssertFalse(state.isActive)
}
func testMutability() {
var state = TimerState(type: .lookAway, intervalSeconds: 1200)
state.remainingSeconds = 600
XCTAssertEqual(state.remainingSeconds, 600)
state.isPaused = true
XCTAssertTrue(state.isPaused)
state.isActive = false
XCTAssertFalse(state.isActive)
}
func testEquality() {
let state1 = TimerState(type: .lookAway, intervalSeconds: 1200)
let state2 = TimerState(type: .lookAway, intervalSeconds: 1200)
let state3 = TimerState(type: .blink, intervalSeconds: 1200)
let state4 = TimerState(type: .lookAway, intervalSeconds: 600)
XCTAssertEqual(state1, state2)
XCTAssertNotEqual(state1, state3)
XCTAssertNotEqual(state1, state4)
}
func testEqualityWithDifferentPausedState() {
let state1 = TimerState(type: .lookAway, intervalSeconds: 1200, isPaused: false)
let state2 = TimerState(type: .lookAway, intervalSeconds: 1200, isPaused: true)
XCTAssertNotEqual(state1, state2)
}
func testEqualityWithDifferentActiveState() {
let state1 = TimerState(type: .lookAway, intervalSeconds: 1200, isActive: true)
let state2 = TimerState(type: .lookAway, intervalSeconds: 1200, isActive: false)
XCTAssertNotEqual(state1, state2)
}
func testZeroRemainingSeconds() {
let state = TimerState(type: .lookAway, intervalSeconds: 0)
XCTAssertEqual(state.remainingSeconds, 0)
}
func testNegativeRemainingSeconds() {
var state = TimerState(type: .lookAway, intervalSeconds: 10)
state.remainingSeconds = -5
XCTAssertEqual(state.remainingSeconds, -5)
}
func testLargeIntervalSeconds() {
let state = TimerState(type: .posture, intervalSeconds: 86400)
XCTAssertEqual(state.remainingSeconds, 86400)
}
}

View File

@@ -0,0 +1,68 @@
//
// TimerTypeTests.swift
// GazeTests
//
// Created by Mike Freno on 1/8/26.
//
import XCTest
@testable import Gaze
final class TimerTypeTests: XCTestCase {
func testAllCases() {
let allCases = TimerType.allCases
XCTAssertEqual(allCases.count, 3)
XCTAssertTrue(allCases.contains(.lookAway))
XCTAssertTrue(allCases.contains(.blink))
XCTAssertTrue(allCases.contains(.posture))
}
func testRawValues() {
XCTAssertEqual(TimerType.lookAway.rawValue, "lookAway")
XCTAssertEqual(TimerType.blink.rawValue, "blink")
XCTAssertEqual(TimerType.posture.rawValue, "posture")
}
func testDisplayNames() {
XCTAssertEqual(TimerType.lookAway.displayName, "Look Away")
XCTAssertEqual(TimerType.blink.displayName, "Blink")
XCTAssertEqual(TimerType.posture.displayName, "Posture")
}
func testIconNames() {
XCTAssertEqual(TimerType.lookAway.iconName, "eye.fill")
XCTAssertEqual(TimerType.blink.iconName, "eye.circle")
XCTAssertEqual(TimerType.posture.iconName, "figure.stand")
}
func testIdentifiable() {
XCTAssertEqual(TimerType.lookAway.id, "lookAway")
XCTAssertEqual(TimerType.blink.id, "blink")
XCTAssertEqual(TimerType.posture.id, "posture")
}
func testCodable() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
for timerType in TimerType.allCases {
let encoded = try encoder.encode(timerType)
let decoded = try decoder.decode(TimerType.self, from: encoded)
XCTAssertEqual(decoded, timerType)
}
}
func testEquality() {
XCTAssertEqual(TimerType.lookAway, TimerType.lookAway)
XCTAssertNotEqual(TimerType.lookAway, TimerType.blink)
XCTAssertNotEqual(TimerType.blink, TimerType.posture)
}
func testInitFromRawValue() {
XCTAssertEqual(TimerType(rawValue: "lookAway"), .lookAway)
XCTAssertEqual(TimerType(rawValue: "blink"), .blink)
XCTAssertEqual(TimerType(rawValue: "posture"), .posture)
XCTAssertNil(TimerType(rawValue: "invalid"))
}
}