Fix ride status visibility, gear rail/readout craft, and hardware bugs - #43
Merged
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7
The ride status sat in the middle of the toolbar, squeezed between the settings and fan controls on one side and Stop on the other. It lost its text long before it lost its icon: a reconnecting trainer showed the rider a bare orange warning triangle 21 points wide with nothing to explain it. The status now sits beside the gear, where it has room to be read, and the toolbar keeps only the gear menu. Stopping used a confirmation dialog, which on iOS 26 renders as a compact card that drops its cancel button entirely. A rider who tapped Stop by mistake mid-ride was left with a destructive choice and no visible way out. It is now an alert, which always shows both actions, and it says what stopping actually does. Both were found by reading rendered screens, and both are now guarded by UI tests with measured assertions rather than mere existence checks.
The gear rail was a row of identical dots with one coloured in, which can only be read by counting. It now fills in the gears already ridden through and gives every marker kind its own height, so a position can be seen rather than counted, and still reads in greyscale. The line under the gear was a fixed large title competing with the number it describes; it is now sized as a caption to it. Easier and harder were two identical slabs distinguished by a small symbol. Harder is drawn solid and easier tinted, so they differ in weight as well as in glyph and word. Neutral grey was tried first and read as disabled, so the lighter button keeps its tint. The four equipment statuses were split three-and-one, orphaning the riding app. They now share a row. A Click battery about to die was a grey caption; it is the one thing on that screen a rider can still act on, so it is drawn as a warning. In settings, a Click that reports itself as "Zwift Click" no longer repeats the row's own title, the gears row leads with the gears chosen rather than a count of them, and cassettes that share a name are told apart by their cog count where no heading does it for them. Retrying a failed start says Try Again rather than Start Shifting, which read as though nothing had been attempted. The rail model, the read-out sizing and the cassette naming are pure functions in the core package with tests, rather than numbers typed into a view. Two more UI tests measure the status row and the battery warning.
Build 9 was archived but never uploaded, so build 10 carries everything since build 8: the per-connection statuses, the ride status that says what it means in words, the stop confirmation a rider can back out of, and the ride screen reworked around what only it can show. The release note records that all of it was made against the simulator and none of it has been ridden on a physical KICKR yet.
Two bugs found riding build 10 on a physical KICKR: 1. The chain-position reminder was only shown in the searching/chooser states, not readyCard, so it vanished the instant the trainer connected and the Start Shifting button jumped upward to fill the space. It now renders in a fixed position across every startup state. 2. DemoModeView.shiftButton constructed ShiftButton without the isProminent parameter added for the ride screen's easier/harder weight differentiation, so demo mode still showed two equally prominent buttons. It now passes isProminent: !easier to match. Test-first: added testTheChainReminderNeverAppearsOrDisappearsAcrossStartupStates and testDemoShiftButtonsAreDrawnLikeTheRideScreensAreWithDistinctWeight (the latter introduces pixel-sampling averageColor/colorDistance helpers since button styling isn't exposed via the accessibility tree), confirmed both red, then fixed. Full suite: 30/30 UI tests, 259 core tests, 90.80% coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7
There was a problem hiding this comment.
Pull request overview
This PR refactors the startup/ride UX to restore an explicit “Start Shifting” step, improves ride-status and equipment-status visibility (including accessibility layouts), adds deterministic UI regression coverage, and tightens CI by enforcing ≥90% core line coverage.
Changes:
- Reworked startup + ride presentation: proxy advertising can start as soon as the trainer is ready; shifting is explicitly started by the rider; ride status is moved to a readable banner near the gear readout; stop confirmation uses an alert with a visible Cancel.
- Added/expanded automated testing: new UI regression suite with debug fixtures + new/updated core unit tests for presentation rules and lifecycle behavior.
- Updated product documentation and release copy across README + docs + App Store notes; CI now runs coverage and UI tests.
Reviewed changes
Copilot reviewed 24 out of 25 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| VirtualGearsUITests/VirtualGearsUITests.swift | Adds deterministic UI regression suite for startup/ride/demo flows, layout, and accessibility identifiers. |
| VirtualGearsProduct/VirtualGearsHomeView.swift | Restores explicit “Start Shifting” flow; makes trainer proxy available when ready; reworks ride status + equipment footer UI and stop confirmation. |
| VirtualGearsProduct/VirtualGearsApp.swift | Expands screenshot/debug fixtures to cover new UI test scenarios and accessibility dynamic type staging. |
| VirtualGearsProduct/SetupView.swift | Improves Settings copy/labels, gear summary presentation, cassette disambiguation for VoiceOver, and adds screen accessibility identifiers. |
| VirtualGearsProduct/FTMSPeripheral.swift | Adds a debug staging helper for “advertising but not connected” screenshot/test scenarios. |
| VirtualGearsProduct/ClickCentralService.swift | Tracks “accepted” physical Click presses for UI feedback and adds debug staging for UI tests. |
| VirtualGears.xcodeproj/project.pbxproj | Adds a UI test target and bumps build number to 10. |
| Tests/VirtualGearsCoreTests/RideReadoutPresentationTests.swift | Pins down rail marker semantics and readout sizing behavior with unit tests. |
| Tests/VirtualGearsCoreTests/ProxyCoordinatorTests.swift | Adds coverage for making the trainer proxy available before shifting begins. |
| Tests/VirtualGearsCoreTests/ProductPresentationTests.swift | Adds/extends unit tests for plain-language connection labels and related presentation invariants. |
| Tests/VirtualGearsCoreTests/HeadwindProtocolTests.swift | Expands protocol and decoding tests; validates mode labeling/control semantics. |
| Tests/VirtualGearsCoreTests/AppConfigurationTests.swift | Adds tests for headwind-forget behavior, virtual gear summary, and real drivetrain naming/range descriptions. |
| Sources/VirtualGearsCore/ProxyCoordinator.swift | Introduces makeTrainerProxyAvailable() and stages additional debug states for screenshots. |
| Sources/VirtualGearsCore/GearRailPresentation.swift | Adds rail marker model + readout metrics logic shared by presentation and tests. |
| Sources/VirtualGearsCore/DrivetrainCatalog.swift | Adds cassette qualifiedName to disambiguate repeated names (UI + VoiceOver). |
| Sources/VirtualGearsCore/AppConfiguration.swift | Adjusts gear summary wording for virtual gears (“extra-low climbing range”). |
| README.md | Updates onboarding instructions to reflect explicit “Start Shifting” step and persistent equipment statuses. |
| docs/requirements.md | Updates usage flow and Click behavior notes to match the new startup/shifting model. |
| docs/index.md | Updates marketing copy to reflect explicit shifting start and Click-press mirroring behavior. |
| docs/demo.md | Updates demo explanation/captions to match new startup/shifting responsibilities. |
| docs/APP_STORE.md | Updates App Store copy and build notes through TestFlight build 1.0 (10). |
| docs/accessibility.md | Updates accessibility documentation for equipment status reflow and Click-press mirroring. |
| DEVELOPMENT.md | Documents coverage requirements and how to run the UI regression suite locally. |
| .github/workflows/ci.yml | Enables code coverage with a ≥90% gate and adds a macOS UI test job. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+438
to
+462
| private func averageColor(of element: XCUIElement) -> (r: Double, g: Double, b: Double) { | ||
| let screenshot = element.screenshot().image | ||
| guard let cgImage = screenshot.cgImage else { return (0, 0, 0) } | ||
| let width = cgImage.width | ||
| let height = cgImage.height | ||
| guard width > 0, height > 0, | ||
| let data = cgImage.dataProvider?.data, | ||
| let pointer = CFDataGetBytePtr(data) else { return (0, 0, 0) } | ||
| let bytesPerPixel = cgImage.bitsPerPixel / 8 | ||
| let bytesPerRow = cgImage.bytesPerRow | ||
| var totals = (r: 0.0, g: 0.0, b: 0.0) | ||
| var samples = 0.0 | ||
| // Sample a sparse grid rather than every pixel: fast, and averages out | ||
| // the icon/text drawn on top of the button's own fill colour. | ||
| let strideStep = max(1, min(width, height) / 12) | ||
| for y in stride(from: 0, to: height, by: strideStep) { | ||
| for x in stride(from: 0, to: width, by: strideStep) { | ||
| let offset = y * bytesPerRow + x * bytesPerPixel | ||
| guard offset + 2 < CFDataGetLength(data) else { continue } | ||
| totals.r += Double(pointer[offset]) | ||
| totals.g += Double(pointer[offset + 1]) | ||
| totals.b += Double(pointer[offset + 2]) | ||
| samples += 1 | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Squashes together this session's work since the last merge to
main:.confirmationDialogsilently dropped Cancel on iOS 26 — replaced with.alert).All test-first: 30/30 UI tests, 259 core tests, 90.80% coverage.
Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com
Copilot-Session: fce4c745-c85c-40aa-aac2-3dbbf605f9e7