import Foundation import Testing @testable import Kanban /// The launch decision and the audit suite's fixture board (10-accessibility.md ▸ Verification). /// /// Two halves, tested for two different reasons. `LaunchPlan.decide` and the flag predicate are /// **pure**, so they are pinned here the way every other launch-time rule in this app is — without a /// `UserDefaults` domain, a live registry, or a running app (`AppModel.shouldRestoreAtLaunch`'s own /// argument, which this composes). /// /// The fixture board is tested here for a blunter reason: **the UI suite that consumes it cannot be /// run in every environment** (it needs Accessibility automation permission and an unlocked /// display), and a fixture that quietly failed to build would turn every audit into a pass over an /// empty screen. Loading it back through the ordinary `BoardLoader` is the one check that runs /// everywhere and would catch that. // MARK: - The launch plan @Suite("The launch plan") struct LaunchPlanTests { /// The three-way decision, exhaustively — the two-way gate `AppModel.shouldRestoreAtLaunch` /// already owns, plus the fixture's outright precedence over both halves of it. /// /// The precedence matters more than it looks: a UI-test launch that also restored the /// developer's flagged boards would open real documents, run real watchers over them, and stamp /// real registry records — during a test run whose whole premise is that nothing outside the /// scratch directory is touched. @Test( "The fixture wins outright; otherwise the restore gate decides", arguments: [ (fixture: true, preference: true, restorables: true, expected: LaunchPlan.uiTestFixture), (fixture: true, preference: false, restorables: false, expected: LaunchPlan.uiTestFixture), (fixture: false, preference: true, restorables: true, expected: LaunchPlan.restoreBoards), (fixture: false, preference: true, restorables: false, expected: LaunchPlan.welcome), (fixture: false, preference: false, restorables: true, expected: LaunchPlan.welcome), (fixture: false, preference: false, restorables: false, expected: LaunchPlan.welcome), ] ) func decision(fixture: Bool, preference: Bool, restorables: Bool, expected: LaunchPlan) { #expect( LaunchPlan.decide( isUITestFixtureLaunch: fixture, restorePreference: preference, hasRestorables: restorables ) == expected ) } /// Which plans need the throwaway bootstrap window: both the ones that open something, neither /// more. Welcome is a scene the app presents directly and needs no view to run a pass for it. @Test("Only the two opening plans present the bootstrap window") func bootstrapPresentation() { #expect(LaunchPlan.welcome.presentsBootstrap == false) #expect(LaunchPlan.restoreBoards.presentsBootstrap) #expect(LaunchPlan.uiTestFixture.presentsBootstrap) } } // MARK: - The flag @Suite("The UI-test fixture flag") struct UITestLaunchFlagTests { @Test("An exact occurrence anywhere in the argument list asks for the fixture") func flagRecognized() { #expect(UITestLaunch.isFixtureLaunch(arguments: ["/path/to/Lanework", UITestLaunch.fixtureFlag])) #expect(UITestLaunch.isFixtureLaunch(arguments: [UITestLaunch.fixtureFlag, "-NSTreatUnknownArgumentsAsOpen", "NO"])) } /// An ordinary launch — including the one XCUITest performs with no arguments of its own — is /// never a fixture launch. @Test("An absent flag is an ordinary launch") func flagAbsent() { #expect(UITestLaunch.isFixtureLaunch(arguments: []) == false) #expect(UITestLaunch.isFixtureLaunch(arguments: ["/path/to/Lanework"]) == false) } /// **Exact match, not a prefix.** A launch switch with a fuzzy edge is a launch switch that can /// be tripped by accident, and this one redirects the registry — the one place an accident would /// look like the user's recents list having been wiped. @Test("A near-miss is not the flag") func flagNotMatchedLoosely() { #expect(UITestLaunch.isFixtureLaunch(arguments: ["\(UITestLaunch.fixtureFlag)s"]) == false) #expect(UITestLaunch.isFixtureLaunch(arguments: ["\(UITestLaunch.fixtureFlag)=basic"]) == false) #expect(UITestLaunch.isFixtureLaunch(arguments: ["-ui-test-fixture-board"]) == false) } /// The flag is double-dashed so `UserDefaults`' `NSArgumentDomain` — which reads `-key value` /// pairs — never sees it as a preference. Stated as a test because the consequence of getting it /// wrong is invisible: the app would work, and a stray defaults key would appear. @Test("The flag cannot be read as an argument-domain preference key") func flagIsNotAPreferenceKey() { #expect(UITestLaunch.fixtureFlag.hasPrefix("--")) } } // MARK: - The fixture board @Suite("The audit fixture board") struct UITestFixtureBoardTests { /// Builds the fixture exactly as a UI-test launch does, then reads it back through the ordinary /// loader — the app's own answer to "is this a board", so the assertion is the same one the /// board window would make. /// /// The scratch directory is prepared first (which is what wipes any previous run's board) and /// removed afterwards, so this test leaves the container as it found it. @Test("It builds, loads, and has the shape the audit suite navigates") func fixtureLoads() throws { UITestLaunch.prepareScratchDirectory() defer { try? FileManager.default.removeItem(at: UITestLaunch.scratchRoot) } let root = try UITestLaunch.materializeFixtureBoard() let model = try BoardLoader.load(boardRoot: root).model // The window title the UI suite waits on. #expect(model.title.value == UITestLaunch.boardTitle) #expect(root.lastPathComponent == "\(UITestLaunch.boardTitle).kanban") // Three lanes, in the order the fixture names them — which is also the order VoiceOver reads // them in (10-accessibility.md ▸ Logical order), so a fixture whose lanes came out shuffled // would make the traversal-order check meaningless. #expect(model.lanes.map(\.title.value) == UITestLaunch.laneTitles) // The first lane is the crowded one, minus the card the fixture deleted — the masonry // divergence the audit is most interested in needs more than one card to diverge. #expect(model.lanes[0].cards.count == UITestLaunch.cardTitles[0].count - 1) #expect(model.lanes[0].cards.allSatisfy { $0.title.value != UITestLaunch.cardTitles[0][UITestLaunch.trashedCardIndex.card] }) // The trashed card is in the trash container and nowhere else — cards only, no lane entries // (03-board-ui.md § Trash). #expect(model.trash.count == 1) #expect(model.trash.first?.title.value == UITestLaunch.cardTitles[0][UITestLaunch.trashedCardIndex.card]) // The rich card: the one the card-window audits open. Its window title is what the UI suite // waits on, its body is what Preview renders into the tree, and its attachment is what the // card element's value and the sidebar's row are made of. let richLane = model.lanes[UITestLaunch.richCardIndex.lane] let richCard = try #require(richLane.cards.first) #expect(richCard.title.value == UITestLaunch.cardTitles[UITestLaunch.richCardIndex.lane][UITestLaunch.richCardIndex.card]) #expect(richCard.body.contains("## What this card is for")) #expect(richCard.attachments == [UITestLaunch.attachmentName]) } /// Everything the fixture launch writes stays inside the app's own container — the sandbox /// constraint that decided the whole design (a path handed over on the command line would not be /// readable), stated as a test so a future "just use `/tmp`" cannot land quietly. @Test("Everything it writes is inside the app container") func scratchIsContained() { let container = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).standardizedFileURL.path #expect(UITestLaunch.scratchRoot.standardizedFileURL.path.hasPrefix(container)) #expect(UITestLaunch.registryStorageURL.standardizedFileURL.path.hasPrefix(container)) #expect(UITestLaunch.fixtureBoardURL.standardizedFileURL.path.hasPrefix(container)) } /// The fixture registry is **not** the real one — the clause that keeps an audit run out of the /// user's recents list. @Test("The fixture registry is not the app's real registry") @MainActor func registryIsRedirected() { #expect(UITestLaunch.registryStorageURL != BoardRegistry.defaultStorageURL) } }