Unit tests cover model behavior, app state, services, pure view helpers, and app lifecycle seams. Prefer small focused test slices while iterating.
AppStateTests.swift- initial state, preferences (including the sort-order and menu-bar counter preferences and their persistence across sign-out),menuBarPendingCountguards (signed out, Off, live selected-list tasks), version/build-commit display, selected list helpers, ordering (Google position and due-date sort), created-task placement (tasksWithCreatedTask), sign-out/disconnect reset, and basic guarded actions.AppStateBehaviorTests.swift- task mutations (including subtask creation landing first despite stale sibling positions, and drag moves being ignored while sorted by due date), caching, stale response protection, selection changes, refresh behavior, errors (including missing-scope bootstrap recovery and preserving sessions on unrelated 403s), notification sync, task-list creation (append/select, blank titles, failure, sign-out during the request), cross-listmoveTask(_:toList:)optimistic updates, rollback, subtask/same-list guards, reminder removal and per-list syncs, and the menu-bar counter's background count sweep, periodic refresh, stale-snapshot discard, and error swallowing (DelayedTasksAPIrecordslistTaskscalls per list, can throw per list, and withsetMoveTaskSuccess()applies same-list and cross-list moves to its own lists while loggingmoveCalls).DemoModeTests.swift- entering/leaving demo mode, sample-list seeding without network access, suppressed notification syncing, credential-preserving sign-out, restoration of the account-backed API on exit, account-wide demo counts for the menu-bar counter, and creating a demo list in memory.MenuBarCounterTests.swift- pure menu-bar counting (open vs due-today, day boundaries, subtasks) and the status item title/length presentation helper.SideBySidePanesTests.swift- two-pane state: preference persistence, secondary default list, per-pane loads and stale-load protection, per-list refresh dedupe, mutation fan-out to every pane showing a list, per-pane mutation targeting, hidden-pane re-seeding, sign-out/demo reset, notification sync per visible list (and never from a pane's empty placeholder before its first fetch lands), the primary-pane forwarders, and moving a task into the other pane's list (uses a recording wrapper aroundDemoTasksAPI).SearchFilterTests.swift- title/notes search, parent context inclusion, root/subtask filtered accessors, and the sort order of filtered roots.TaskItemModelTests.swiftandGoogleTasksAPITests.swift- model Codable round trips, completion helpers, parent/subtask fields, and due-date accessors.GoogleTasksAPIBehaviorTests.swift- REST request methods, query parameters (including the move endpoint'sdestinationTasklist), pagination, auth headers, create/update bodies, the task-list creation request, error mapping (including modern/legacy scope failures and unrelated 403 preservation),setTaskCompleted's status-only PATCH (URL, method, auth header, body, decode, realistic Google IDs, and 401/404/429/500/network error mapping), andGoogleTasksAPIworking against a fakeAccessTokenProvidinginstead of the concreteGoogleAuthService.GoogleTasksAPILiveTests.swift- opt-in contract checks against the real Google Tasks API: the first-child insert semanticsAppState.addSubtaskrelies on (positions come back00000000000000000000and siblings are renumbered),AppState.addSubtaskitself showing the server's order before any refresh, and whethertasks.movewithdestinationTasklistcarries a parent's subtasks along (the assumption behindAppState.moveTask(_:toList:)). Skipped unlessTASKMENU_LIVE_GOOGLE_TESTS=1is set (pass it asTEST_RUNNER_TASKMENU_LIVE_GOOGLE_TESTS=1toxcodebuild) and the test host can read a sign-in, which means signing in once from a Debug run of the app; it works in a throwaway task list that it deletes afterwards, or insideTASKMENU_LIVE_GOOGLE_LIST_ID(tasks left in place) for comparing with the Google Tasks website.GoogleAuthServiceTests.swift- token loading, refresh/sign-in callback behavior, token exchange errors, partial-consent rejection and granted/omitted scope compatibility, revocation, web-auth test doubles, sign-out clearing both the current and injected legacy Keychain,initinvoking a providedKeychainMigrationCoordinating, andAccessTokenProvidingconformance.KeychainServiceTests.swift- production wrapper behavior, XCTest in-memory isolation, and access-group isolation/round-trip/delete behavior (a shared-group instance and a legacy no-group instance sharing the sameservicestring never see each other's data).WidgetKeychainMigrationTests.swift-KeychainMigration's per-keyKeychainItemMigrationStateoutcomes againstInMemoryKeychainServicedoubles: no-op when the shared group is unresolved, fresh shared save/read/delete, a legacy-only install migrating every known key (and the resultingGoogleAuthServicestaying signed in), shared-copy-wins when both exist, resuming a partial migration safely, a failed shared write leaving the legacy item intact, an unverifiable shared write leaving the legacy item intact, migration touching only the exact known keys, and sign-out removing both locations. Includes a localScriptedKeychainServicedouble for scripting write/verification failuresInMemoryKeychainServicecannot express. Three signed-host regressions bypass the in-memory path and use unique disposable Keychain services to verify repeated startup, legacy upgrade, and shared-copy-wins cleanup against real Security framework access-group semantics. They skip when the host lacks the shared entitlement and never use the production OAuth service.WidgetGoogleAccessTokenProviderTests.swift-WidgetGoogleAccessTokenProvideragainst a shared-groupInMemoryKeychainServiceandMockURLProtocol: no-credentials/no-network-call, cached-valid-token reuse, expired-token refresh writing shared access/expiration, an omitted vs. rotated replacement refresh token, transient 429/400-undecodable/network failures preserving credentials, definitiveinvalid_grant/invalid_clientclearing shared credentials, same-process concurrent-caller coalescing onto one refresh, and that the provider (never given a legacy-location double) cannot read legacy credentials.DueDateNotificationServiceTests.swift- authorization, scheduling, stale-removal, and identifier targeting.GitHubUpdateCheckerTests.swift- semantic-version parsing, GitHub release decoding, update-check persistence, throttling, launch-alert suppression, and AppState update outcomes.DateFormattingTests.swift- RFC 3339, Google due-date, display, and relative-date behavior.TaskWidgetSnapshotStoreTests.swift-TaskWidgetSnapshotStore's coordinated App Group reads/writes: missing/corrupt/future-schema files, atomic replacement, concurrent app/extension-style writers,replaceList(_:requestStartedAt:)stale-write rejection and acceptance, unrelated-list preservation,pruneLists(keeping:)deleted-list removal,clear()sign-out behavior, an unavailable container, and a check that the serialized JSON never contains a notes/email/token key. Every test uses an injected temporary directory, never the real App Group.TaskWidgetProjectionTests.swift-TaskWidgetProjection's open-only filtering, Google-position/due-date root ordering (including position ties), direct-subtask placement and the orphan-subtask-becomes-root policy, row-budget truncation and+Nremainder counting, and due-treatment bucketing (overdue/today/tomorrow/dated/undated) across multiple time zones and DST boundaries.TaskWidgetProviderTests.swift-TaskWidgetTimelineLoaderandTaskWidgetListCatalog, the Foundation-only decision logic behind the extension'sTaskListWidgetProvider/TaskListEntityQuery(which live only inTaskMenuWidgetand are not visible to this test host — seeTaskMenuWidget/README.md's "Testability boundary"): placeholder sample content performing zero I/O and honoring the row budget, cache-only reads for signed-out/no-cache/populated-stale gallery content, live success writing and returning fresh tasks, offline/cancellation falling back to cache without persisting an error, no-cache-and-offline rendering "no cache" rather than crashing, definitive auth rejection clearing credentials and cached task titles, a deleted configured list staying unavailable, and the entity catalog's identity/order/default-list/renamed-title resolution. Also hosts the sharedFakeTasksAPIdouble used byCompleteTaskIntentTests.swift.TaskWidgetDeepLinkTests.swift-TaskWidgetDeepLinkconstruction/parsing only: valid round trip, percent-encoded ids, wrong scheme/host/path, missing/empty/duplicate/oversizedid. App-side routing (popover presentation, cold-start buffering) is out of this suite's scope.TaskWidgetDeepLinkRoutingTests.swift- the widget deep-link routing decisions against fakes (noNSAppleEventManager, AppKit event plumbing, or live status item):TaskWidgetDeepLinkRoutermalformed/wrong-scheme URLs having no side effects, signed-out routing straight to the popover, signed-in-but-not-loaded bootstrapping before selecting, a valid list selecting in the primary pane, and a deleted/unknown list presenting the popover without switching lists; plusDeepLinkColdStartBufferTestscovering the "hold at most one link, replace rather than queue a second one, flush exactly once onmarkReady()" contract.CompleteTaskIntentTests.swift-TaskWidgetCompletionCoordinator, the Foundation-only cascade/refetch/publish logic behind the extension'sCompleteTaskIntent(not visible to this test host — seeTaskMenuWidget/README.md): single-task success, parent-to-open-children cascade, subtask completion never affecting siblings, total transient failure leaving the cache unchanged, partial child failure publishing authoritative server truth instead of claiming full success, a stale in-flight app fetch unable to resurrect a task the widget already completed, definitive auth failure clearing credentials and the cache, and refusing to mutate a task id the cached list never confirmed.AppStateWidgetPublishingTests.swift-AppState's widget-snapshot publishing at every convergence point (bootstrap/list load, selected-list load, account-wide sweep, add/edit/delete/reorder, completion and its rollback, cross-list move and its rollback, list creation, deleted-list recovery, selection change, sign-out/disconnect), demo mode never reaching a production publisher, and the cross-process stale-write protectionTaskWidgetSnapshotPublisherprovides against a slow in-flight fetch overwriting a newer cross-process write. Most tests inject the fast in-memoryRecordingTaskWidgetSnapshotPublisher; two tests use a real store-backedTaskWidgetSnapshotPublisherpointed at a temporary directory (never the App Group) to prove store-level behavior, including that the published JSON contains no notes/email/token content.TaskListViewTests.swift,TaskDetailViewTests.swift, andMenuBarWindowChromeTests.swift- shared task presentation helpers (including the pane-awareTaskListPresentationvariants), the inline add-subtask field and where a committed subtask renders (end to end throughAppStateandDemoTasksAPI), the header's sort menu and side-by-side toggle button, drag gating under a due-date sort, the list picker's "New List…" item and inline field, the "Move to" context submenu, the content view rendering the pane it is handed, the task editor's list picker (ID-based selection, enabled/disabled rules, and a move that saves pending edits first), the signed-in popover rendering one or two pane controllers at 320 or 641 wide with the hairline divider, and AppKit glass/window behavior, including the Reduce transparency Settings switch, preference persistence, and live surface updates without rebuilding controls.TaskMenuActionButtonTests.swift- pointing-hand cursor opt-in onTaskMenuActionButton(cursor tracking areas andcursorUpdatebehavior).MetricKitPayloadStoreTests.swift- local payload persistence.TaskMenuAppTests.swift- app/app-delegate construction seams, launch UI mode parsing, the automatic update-check loop, and update-alert choice mapping.StatusBarControllerPopoverTests.swift-PopoverPresentationDecision.shouldPresent, the pure idempotency guard behindStatusBarController.showPopover()(the widget deep-link entry point): presents when not already shown, does nothing when already shown so a second call never toggles the popover closed.StatusBarControlleritself keeps itsNSStatusItem/NSPopoverprivate and is not constructed live in this suite; see the file's header comment.TaskMenuMainMenuTests.swift- main-menu structure and ordering, first-responder selectors and nil targets, key-equivalent resolution for every shortcut, autoenabling, responder-chain resolution of ⌘N/⌘F from a focused text field, and the unconditional launch install. Never performs a realcopy:— that would clobber the developer's clipboard.SettingsLaunchAtLoginTests.swift- the pure launch-at-login status decision (requiresApprovalnotice) behind the Settings toggle.SettingsVersionRowTests.swift- the rendered Settings version row, covering theVersion <version> (<commit>)text and thedevfallback for unstamped builds.SettingsSideBySideRowTests.swift- the "Show two lists side by side" switch in Settings › General: present, mirrorsAppState.sideBySideListsEnabled, and writes it back.
MockURLProtocol.swiftprovides a URLSession that records requests and returns stubbed responses.InMemoryKeychainService.swiftprovides keychain success/failure doubles.InMemoryKeychainServicetakes an optionalaccessGrouplabel (each instance is already an independent store, so construct one per location — e.g. one for "legacy" and one for "shared" — to test access-group-aware code without the real Keychain).TestDueDateNotificationService.swiftrecords notification sync and removal calls forAppState.
xcodebuild test -project TaskMenu.xcodeproj -scheme TaskMenu \
-configuration Debug \
-only-testing:TaskMenuTests/GoogleTasksAPIBehaviorTests- Use
-only-testing:for the nearest suite first. - Mark test classes
@MainActorwhen they touchAppState,GoogleAuthService, AppKit, or other main-actor helpers. - Keychain tests must use unique service names or the in-memory XCTest path to avoid cross-test contamination.
- The default widget publisher isolation test must pass with signed and unsigned hosts; a resolved App Group identifier is valid, but the publisher's store must remain container-less under XCTest.
- When changing source file membership, regenerate with
xcodegen generatebefore running tests.