diff --git a/Shared/Periscope/TODOs.md b/Shared/Periscope/TODOs.md index 18d4f5ba..6a125a9f 100644 --- a/Shared/Periscope/TODOs.md +++ b/Shared/Periscope/TODOs.md @@ -41,6 +41,7 @@ here. - feat(PeriscopeTools) [needs-design]: Inspect-by-object is scope-granular, not instance-granular. `.logInspectable(_:)` keys the badge/inspector to a `Log`'s *scope*, so tagging a list row (Where tags `EvidenceRow` with `WhereLog.evidence`, `LocationStatusRow` with `WhereLog.session`) surfaces the whole scope's recent events, not that one row's. Events already carry `externalID` for object correlation, but the inspector can't filter by it — a per-instance child scope (blocked on the `LogContextProviding` parent-hierarchy P0) or an `externalID`-scoped inspect entry would make true row-/object-level inspection work. (pr#94 review) - design(PeriscopeCore) [needs-design]: No eager store handle — `PeriscopeStore.make` being `async` forces an "optional store, observe until it lands" dance on consumers. Where exposes an `Optional` on `WhereModel` that stays `nil` until the bootstrap `Task` completes, and `RootView` has to watch the transition (`.onChange` of the store identity) to wire the viewer/inspector/alerter. A synchronous pending-store handle (usable immediately, resolves in the background) or an `await`-readiness accessor would remove the optional-and-observe boilerplate every app repeats. (agent) - test(PeriscopeTools) [quick-win]: Add the missing 1:1 tests for the small `*+Display` extensions. (audit 2026-07-26) +- test(PeriscopeTools) [needs-design]: broken-snapshots — replace the hosting smoke tests with image snapshots. Eighteen tests across nine files assert nothing but "the hosted view reached a window": `#expect(await waitUntil { host.view.window != nil })` in `LogEventListTests.swift:30`, `:41`, `LogHierarchyViewHostingTests.swift:23`, `:34`, `PeriscopeViewerHostingTests.swift:29`, `:42`, `ScopeEventsViewHostingTests.swift:25`, `:38`, `SpanHistoryViewHostingTests.swift:23`, `:34`, `SpanTreeViewHostingTests.swift:26`, `:37`, and the `try waitFor { host.view.window != nil }` spelling in `LogInspectableHostingTests.swift:25`, `:38`, `:50`, `LogTraceViewHostingTests.swift:23`, `OpenSpansViewHostingTests.swift:27`, `:38`. The predicate restates what `show`/`showHosted` already guarantee, so each test proves only that construction didn't crash — never what rendered, which is the part the elaborate seeding sets up (`LogHierarchyView`'s outline, the comfortable density `PeriscopeViewerHostingTests` injects, the "No Events" state `ScopeEventsViewHostingTests` documents at `:29`). The repo convention is now that an image bundle, not a hosting smoke test, owns "does this screen render" (see [`Where/WhereUI/AGENTS.md`](../../Where/WhereUI/AGENTS.md#testing) and the WhereUI suite that replaced its own smoke tests). Convert them to a `PeriscopeToolsSnapshotTests` bundle over the same seeded stores, keeping any assertion that isn't the window check and deleting the files left empty. The bundle is the prerequisite tracked in the root [`TODOs.md`](../../TODOs.md) broken-snapshots item, which also wants `PeriscopeViewer`'s snapshot moved out of WhereUI — land them together. (Note the two `window != nil` checks in `Shared/LifecycleKit/Tests/` are *not* in scope: they assert the hosting helper's own lifecycle contract, which is the one place the check is the point.) (pr#101 review) # Completed issues diff --git a/Shared/SwiftDataInspector/TODOs.md b/Shared/SwiftDataInspector/TODOs.md index b6f0e678..d799f1db 100644 --- a/Shared/SwiftDataInspector/TODOs.md +++ b/Shared/SwiftDataInspector/TODOs.md @@ -9,5 +9,6 @@ here. ## P1s (Should do) - test [quick-win]: The bare-`PersistentIdentifier` branch of relationship resolution (`SwiftDataReflection.swift:132`) is untested — the existing relationship test faults a materialized model, which takes the other path. Add a fixture test over an unmaterialized slot. (audit 2026-07-26) - test [needs-design]: `Tests/SwiftDataInspectorTests.swift` is one 759-line file covering 13 sources, against the 1:1 convention. Split it by concern, optionally adding hosted UI smoke tests. (audit 2026-07-26) + - test [needs-design]: broken-snapshots — don't add those hosting smoke tests; take image snapshots instead. The repo convention is now that a `*SnapshotTests` bundle owns "does this screen render", and a "hosts without crashing" test asserts nothing about the result (see the conversion item in [`Shared/Periscope/TODOs.md`](../Periscope/TODOs.md)). This module has no snapshot bundle yet — adding one is tracked in the root [`TODOs.md`](../../TODOs.md) broken-snapshots item, which also wants `SwiftDataInspectorSnapshotTests` moved here out of `WhereUI`. Do the split against that bundle rather than seeding smoke tests it would immediately replace. (pr#101 review) # Completed issues diff --git a/TODOs.md b/TODOs.md index cb584604..42500ac9 100644 --- a/TODOs.md +++ b/TODOs.md @@ -98,6 +98,9 @@ inbox rather than here. ## P1s (Should do) ## P2s (Nice to have) +- test(WhereUI) [needs-design]: broken-snapshots — two `WhereUISnapshotTests` suites pin views WhereUI doesn't own, flagged `[Fix later]` on PR #101. Each belongs to the module that owns the view, so both span WhereUI and a shared module and sit here rather than in either file. Both are blocked on the same prerequisite: neither shared module has a snapshot bundle yet, and adding one means a `*SnapshotTests` target plus its own standalone scheme and a `snapshot` CI job, following the `WhereUISnapshotTests` pattern in `Project.swift` — image bundles are deliberately kept out of `Stuff-iOS-Tests`. (pr#101 review) + - test(PeriscopeTools) [needs-design]: Move the `PeriscopeViewer` snapshot into Periscope. `Where/WhereUI/SnapshotTests/PeriscopeViewerSnapshotTests.swift` captures a PeriscopeTools view, and its own doc comment (`:8`–`:11`) admits it lives in Where only because that's where the app reaches it; it already builds its own frozen store (`:34`), so nothing but the `whereBroadwayRoot()` styling ties it here. Pairs with the hosting-test conversion in [`Shared/Periscope/TODOs.md`](Shared/Periscope/TODOs.md), which needs the same bundle — do them together. (pr#101 review) + - test(SwiftDataInspector) [needs-design]: Move the `SwiftDataInspector` snapshot into SwiftDataInspector. `Where/WhereUI/SnapshotTests/SwiftDataInspectorSnapshotTests.swift` reaches through `PreviewSupport.previewServices()`, a seeded manual day, and a real `WhereSession` just to obtain a configuration (`:12`–`:21`) — so an app-agnostic module is snapshotted through Where's store. The module's own in-memory `@Model` fixtures would render it without the app, matching how its unit tests already work. (pr#101 review) - perf(StuffTestHost) [needs-design]: Two loose ends in the shared test host, both reaching the root Tuist manifest, which is why they sit here rather than in a StuffTestHost file. The WhereCore-always-embedded build trade-off is documented and verified load-bearing at `Project.swift:256` — decide whether to keep documenting it or split the host so unrelated bundles don't pay for it. Separately, the scene configuration name is spelled twice, in `Shared/StuffTestHost/Sources/AppDelegate.swift:11` and `Project.swift:244`, so the two can drift silently. (audit 2026-07-26) # Completed issues diff --git a/Where/TODOs.md b/Where/TODOs.md index 38332ee7..2d0bf3a1 100644 --- a/Where/TODOs.md +++ b/Where/TODOs.md @@ -53,6 +53,15 @@ The item format and the placement rule live in the root - refactor(WhereCore): Persist a **minReaderVersion** per entity, not just a version. Additive (expand/contract) changes leave it low so old builds keep reading via the retained old field (tolerant reader); only a genuinely forward-incompatible change bumps it. Readers exclude entities whose `minReaderVersion > appVersion` and surface a "some data needs a newer app" warning — the only case that actually needs exclusion. (agent) - refactor(WhereCore): Durable write-back is **read-repair**, decoupled from read correctness: opportunistically (batched, on `.NSPersistentStoreRemoteChange` + launch) rewrite stale records to the current version and stamp it, so old builds can honor exclusion. Transforms must be deterministic + commutative so two devices healing the same record via CloudKit converge (LWW-safe). (agent) - design(WhereCore): Open question — the exclusion UX, where an older device progressively hides days a newer device has touched, needs a deliberate warning surface, not a silent drop. (agent) +- fix(WhereUI) [needs-design]: broken-snapshots — the snapshot suite pinned genuinely broken renderings as references, flagged with `[Fix later]` review comments on PR #101 and merged anyway to land the suite. These are not flaky captures (those have their own ledger below) — each is a faithful, reproducible image of something actually wrong, so re-recording is never the fix. Fix the view, the capture frame, or the pipeline as each item says, then re-record just that reference under `Where/WhereUI/SnapshotTests/__Snapshots__/`. Most cluster on the accessibility axes `.screenDefaults` added — the ax5 Dynamic Type and VoiceOver-annotated configurations that nothing rendered before this suite existed. (pr#101 review) + - fix(WhereUI) [needs-design]: broken-snapshots: the VoiceOver-annotated calendar captures are blank. `CalendarContentViewSnapshotTests/calendarContent.WithData_iPhone_accessibility.png` (66 KB) and `..._iPad_accessibility.png` (171 KB) are solid white inside their border, against 205 KB–2.2 MB for every other screen-sized `_accessibility` reference — so it's specific to `CalendarContentView`, not the annotation pipeline. `AccessibilitySnapshotViewController` renders the wrapper with `viewRenderingMode: .drawHierarchyInRect` (`Shared/SnapshotKitTesting/Sources/AccessibilitySnapshotViewController.swift:36`), and `parseAccessibility()` claims failures "surface loudly rather than producing a blank image" (`:44`) — whatever this is slips past every `ImageRenderingError` guard there. Two configurations' worth of accessibility coverage currently assert a blank image, so a real regression in them can't fail. (pr#101 review) + - fix(WhereUI) [quick-win]: broken-snapshots: the calendar day grid breaks at accessibility Dynamic Type. Every two-digit date truncates to its first digit — the 10th–31st render as "1", "2", or "3" — because the day number is clamped to a fixed square (`CalendarContentView.swift:436`, `.frame(width: calendar.day.numberSize, height:)`), and the weekday header row wraps mid-word ("Sun" over two lines, "Wed" over three) because each symbol is a plain `Text` in an equal-width grid column (`:282`). Both show in `calendarContent.WithData_iPhone_ax5.png`; the digit truncation also hits `..._iPad_ax5.png`, where the extra width goes to inter-column gaps instead of the numbers. Showing "1" where the date is 10 is wrong content, not merely tight layout. (pr#101 review) + - fix(WhereUI) [quick-win]: broken-snapshots: the presence timeline's leading accent bar doesn't scale with Dynamic Type. `StintRow` sizes its `Capsule` from the fixed stylesheet tokens `timeline.accentWidth`/`accentHeight` — 4×34 (`Sources/Shared/WhereStylesheet.swift:719`, `:720`) applied at `PresenceTimelineList.swift:54`–`:57` — so at ax5 it stays a 4pt stub beside ~40pt text (`presenceTimeline.WithData_iPhone_ax5.png`, `..._iPad_ax5.png`). Scale it with `@ScaledMetric`, or derive it in `WhereStylesheet.init(context:)` the way the day-grid tap targets already do. (pr#101 review) + - fix(WhereUI) [needs-design]: broken-snapshots: the presence timeline row squishes horizontally at ax5 instead of restacking. `StintRow` keeps accent bar, emoji, name/date stack, and day count on one `HStack` line at every type size (`PresenceTimelineList.swift:51`), so `presenceTimeline.WithData_iPhone_ax5.png` renders "California" hyphenated over three lines beside a two-line "148 days". Switch to a `ViewThatFits` / `AnyLayout` that stacks vertically at accessibility sizes. (pr#101 review) + - fix(WhereUI) [needs-design]: broken-snapshots: `YearView` overflows horizontally at ax5. In `year.Loaded_iPhone_ax5.png` the month title reads "nuary", the day grid is clipped on both edges, and the Calendar/Timeline pill runs off the trailing edge. The suspect is `YearModePicker`, a fixed-width `HStack` of two labelled segments pinned as a bottom `safeAreaInset` (`YearView.swift:36`–`:38`, `:79`), which is wider than the screen at ax5. Confirm the oversized inset is what widens the layout beneath it, then make the picker fit at accessibility sizes (icon-only, wrapped, or scrollable). (pr#101 review) + - fix(WhereUI) [quick-win]: broken-snapshots: the Resolve toolbar badge sits awkwardly on the iOS 26 glass toolbar button. `ResolveToolbarLabel` hand-rolls the badge as a red `Capsule` overlaid on the `checklist` symbol and pushes it out with a fixed `.offset(x: spacing.small, y: -spacing.small)` (`LocationsView.swift:210`–`:217`), landing it half outside the button's own glass capsule — visible in `root.LoggedIn_iPhone.png`. Use SwiftUI's `.badge()` on the toolbar item, or offset against the resolved chrome rather than a fixed spacing token. (pr#101 review) + - test(WhereUI) [quick-win]: broken-snapshots: the Resolve sheet's ax5 reference is cut off mid-content. `resolution.WithIssues_iPhone_ax5.png` slices the second issue card's subtitle at the frame's bottom edge, because the case captures at the fixed `.iPhone` device frame via `.screenDefaults` (`ResolutionView.swift:221`), which clips rather than growing. `SnapshotConfiguration.Frame.fullContent` exists for exactly this (see `CalendarContentView.swift:589`) — add a full-content ax5 case so the whole sheet is pinned instead of its first screenful. (pr#101 review) + - fix(WhereUI): broken-snapshots: `locations.Loaded_iPad.png` bakes in raw inflection markup — the Elsewhere card's subtitle renders literally as `^[3 region](inflect: true)`. This is the `locations.elsewhere.subtitle` P1 filed above, now pinned as a reference; recorded here so the image isn't mistaken for correct output, and so that reference is re-recorded when the fix lands. (pr#101 review) ## P2s (Nice to have) - feat(WhereUI) [needs-design]: Give the app a branded launch screen. `UILaunchScreen` is an empty dictionary (`Project.swift`), so the pre-main frame is plain white. Measured from a fresh-install simulator recording, a first run reads as ~1.7s of white → ~0.25s of the dark `LaunchSplashView` → the light onboarding screen, so the splash registers as a quarter-second dark blip between two light screens rather than as the app opening. A launch screen matching the splash's background + icon would make that continuous. Note this is the right layer to fix it at: the splash's own `minimumSplashDuration` hold deliberately gates only the `.ready` reveal, not a gate transition like onboarding, so lengthening the hold would just delay interactive UI. (agent)