diff --git a/App/AppLauncher.swift b/App/AppLauncher.swift new file mode 100644 index 0000000..78bc748 --- /dev/null +++ b/App/AppLauncher.swift @@ -0,0 +1,27 @@ +import SwiftUI + +/// Entry point: unit tests get an empty host app, so launch work (database, sync, menu bar) never runs under XCTest. +@main +enum AppLauncher { + static func main() { + if isRunningUnitTests { + UnitTestHostApp.main() + } else { + StudyAppApp.main() + } + } + + private static var isRunningUnitTests: Bool { + let environment = ProcessInfo.processInfo.environment + return environment["XCTestConfigurationFilePath"] != nil + || environment["XCTestBundlePath"] != nil + || environment["XCTestSessionIdentifier"] != nil + } +} + +/// Host app for the unit tests; the tests build what they need themselves. +private struct UnitTestHostApp: App { + var body: some Scene { + WindowGroup { EmptyView() } + } +} diff --git a/App/StudyAppApp.swift b/App/StudyAppApp.swift index 3667acf..4416918 100644 --- a/App/StudyAppApp.swift +++ b/App/StudyAppApp.swift @@ -3,7 +3,6 @@ import AppIntents import CoreSpotlight import SwiftUI -@main struct StudyAppApp: App { private let container: AppContainer private let autopilot: LectureAutopilot diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 85029e8..0ce1b73 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -6,7 +6,7 @@ StudyApp follows a pragmatic Clean Architecture (ports and adapters). The goal i ```text ┌──────────────────────────────────────────────────────────────┐ -│ App StudyAppApp (@main) · AppContainer · AppUpdater │ wires everything together +│ App AppLauncher (@main) · StudyAppApp · AppContainer │ wires everything together ├──────────────────────────────────────────────────────────────┤ │ Presentation SwiftUI views · @Observable view models │ ├──────────────────────────────────────────────────────────────┤ @@ -26,7 +26,7 @@ Dependencies point **inward**: Presentation and Data both depend on Domain, Doma | Presentation | `Presentation/` | Domain, SwiftUI, AppKit, WebKit | One folder per feature with views and view models; `DesignSystem/` for shared components and `Theme` | | Shared | `Shared/` | Foundation | Data contract between app and widget extension (`WidgetSnapshot`, `StudyAppLink`) | | Widgets | `Widgets/` | Shared, WidgetKit, SwiftUI | Widget extension; reads the snapshot only | -| App | `App/` | everything | `AppContainer` (composition root), `StudyAppApp`, `AppUpdater` | +| App | `App/` | everything | `AppContainer` (composition root), `AppLauncher`, `StudyAppApp`, `AppUpdater` | ## Composition root @@ -34,6 +34,8 @@ Dependencies point **inward**: Presentation and Data both depend on Domain, Doma `AppContainer(inMemory: true)` swaps in in-memory persistence and preferences for previews and tests. +`AppLauncher` is the entry point: under XCTest it starts an empty host app instead of `StudyAppApp`, so launch work (database, sync, menu bar item) never runs during unit tests. On CI runners the menu bar scene kept the test host from finishing its launch. + ## Data flow example: calendar subscription → module 1. `StudyCalendarViewModel.subscribe(to:)` normalises the address (`CalendarFeedURL`) and calls `SyncCalendarSubscriptionUseCase`.