tapUntilGone gated the save tap solely on isHittable. Under CI load the
top-bar save button's accessibility frame can never settle to hittable
within the timeout, so the loop waited out the whole budget without ever
tapping and failed with 'Element still present after 20.0s'.
Keep the proven isHittable hit-tested fast path and add a geometry
fallback: once the button's frame has settled (fully below the status
bar, ruling out the transient dead-pixel position, and stable across two
samples) tap it via tapCenter. This guarantees a tap eventually fires
even when isHittable never flips true, while still avoiding the
dead-pixel/scroll-away hazard the isHittable gate originally fixed.
A dropped coordinate tap left the field unfocused, so the software
keyboard never rose and the test failed on the single 10s wait.
Re-focus until the keyboard appears, matching typeIntoEditor.
* Harden flaky iOS scene-editor UI test with tap-retry helpers
SceneEditorWorkflowUITests.testEditSceneTextThenSave intermittently timed
out (most visibly waiting 20s for the save affordance to disappear). The
UI is Compose Multiplatform, which renders to a single surface, so
XCUITest taps are best-effort coordinate taps onto that surface. An
individual tap can be silently dropped (mid-relayout, under simulator
load, or while the software keyboard geometry is shifting). A lone tap
followed by one long wait is the classic XCUITest flake: a single dropped
tap costs the whole timeout and fails the test.
Add a small retry primitive and route the flake-prone taps through it:
- tapUntil(element, until:) re-taps on a short sub-timeout until the app
demonstrably reacts, converging instead of eating the full timeout on
one dropped tap.
- tap(_:expecting:) drives each scene-creation step by the element its
tap surfaces (add menu, then the create-item dialog).
- tapUntilGone(_:) replaces the lone save tap + long wait; it also covers
a late IME keystroke re-dirtying the buffer right after a save.
- openProjectCard re-taps the card until the project root's nav rail
renders (same dropped-tap resilience), keeping its existing
name/firstMatch card selection.
Validated by running the full iosUITests scheme on freshly-erased
simulators across 6 consecutive runs (all four UI test classes green
every run).
* Fix save-tap flake: only tap the save button when it's settled/hittable
CI surfaced the actual flake this test was meant to harden: the save tap
timed out because the button's accessibility frame is briefly stale right
after the edit that surfaces it — reported up under the status bar (a dead
pixel) until the top-bar layout settles. Blindly re-tapping that stale
coordinate never saved and could even scroll the app away from the editor,
so the re-tap loop ran the full timeout and failed.
Make tapUntilGone tap only when the button reports itself hittable
(settled), using a hit-tested tap() that re-resolves its real position;
otherwise wait for it to settle. Left untouched, the button stays hittable
even under load, so this converges cleanly instead of fighting a dead spot.
Scoped to the save helper; the navigation/card taps are unchanged.
Validated on a CI-sized simulator (iPhone 16e, 390x844) across 6 runs
under CPU load, all green.
The iOS SceneEditorWorkflowUITests was scoped to "scene opens" rather than
the Android-parity edit->save flow because the scene-editor-save affordance
never surfaced to XCUITest. Root cause: the iOS EditorTopBar save IconButton
was missing the SCENE_EDITOR_SAVE_TAG testTag that the Android/desktop
variants carry, so even once the edit dirtied the buffer and the button
rendered, XCUITest had no accessibility identifier to find.
- Tag the iOS EditorTopBar save button with SCENE_EDITOR_SAVE_TAG.
- Add a typeIntoEditor(_:into:until:) helper to HammerUITest that re-focuses
and re-injects until the edit propagates, mirroring the Android
typeIntoEditor retry loop (guards the enabled=hasReceivedInitialBuffer
gate on the initial buffer load).
- Promote testCreateSceneOpensEditor to testEditSceneTextThenSave: type into
the editor, assert save appears, tap it, assert it disappears — matching
SceneEditorWorkflowTest.editSceneTextThenSave.
Verified on iPhone 16 (iOS 18.6) simulator: TEST SUCCEEDED.
* Add iOS UI smoke tests (XCUITest)
Adds an iOS UI smoke-test suite that drives the real app on a simulator via
XCUITest, the iOS analogue of the android/src/androidTest Compose UI tests.
The whole iOS UI is Compose Multiplatform, and CMP (1.8+) maps Compose testTags
to iOS accessibilityIdentifiers automatically, so the tests target the same
testTags the Android suite uses.
Workflows covered (all green on simulator):
- LaunchSmoke: app boots through the SwiftUI entry point, Koin + data migration
run, project selection renders.
- ProjectWorkflow: create + open a project (exercises the create dialog text
entry and navigation into the editor).
- SceneEditorWorkflow: create a scene and confirm it opens in the editor.
- NotesWorkflow: navigate to Notes and open the create-note screen.
Setup:
- ios/scripts/add_ui_test_target.rb idempotently creates the iosUITests
UI-testing target + shared scheme via the xcodeproj gem (the folder previously
had source files but no actual target).
- ios/scripts/disable_sim_hardware_keyboard.sh forces the software keyboard so
XCUITest text entry lands (Compose fields need it).
- ios-ui-tests CI job runs the suite on a simulator and uploads the xcresult on
failure.
- composeUi ProjectCreateDialog: tag the name field so the create flow is
targetable (also benefits Android).
Known limitation: the app's custom rich-text editors (scene body, note body via
MarkdownEditField) do not report keyboard focus to XCUITest, so their text entry
can't be driven (only standard Compose text fields can). The scene and notes
tests therefore stop at "editor opens" / "creation screen opens"; the full
edit/create-with-body paths remain covered by the Android suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* iOS UI tests: type into note body via composetexteditor 2.3.0
composetexteditor 2.3.0 publishes text-editing accessibility semantics on its
editor, so XCUITest can now drive text entry into it. Bump the dependency and
promote the Notes UI test to the full Android-parity flow: create a note by
typing into the body, then assert its card appears.
The scene edit+save flow stays scoped to "scene opens" for now — text entry
works, but the scene editor's initial-buffer gating + dirty-driven save make
the save affordance unreliable to assert from an IME-driven edit; promoting it
is a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace ad-hoc icon scripts with a single source-of-truth manifest
(scripts/assets.yaml) and a Python generator that renders every app icon,
store-listing graphic, MSIX tile, favicon, and the Play feature graphic from
two SVG sources. Compositions (icon + "Hammer" wordmark in Kingthings
Trypewriter) are defined once as percentages of canvas and reused across the
Play feature graphic, MSIX wide/splash tiles, and the Snap featured banner.
Adds the missing snap store icon, fixes the snap desktop icon to the correct
256x256 hicolor size, and routes uploads-only assets to build/store-assets/
(gitignored).
See docs/ASSET-GENERATION.md for the manifest schema.
Declare ITSAppUsesNonExemptEncryption=false so App Store Connect stops
asking on every upload, drop the legacy armv7 UIRequiredDeviceCapabilities
entry, and align CFBundleShortVersionString with the shared codebase
version (3.0.3) in gradle/libs.versions.toml. pbxproj now references the
freshly-issued "Hammer AppStore iOS" provisioning profile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iOS now runs the same shared Compose UI as Android and Desktop. The
existing Decompose component graph (IosRoot, ProjectSelection,
ProjectRoot) is reused; the Swift app shrinks to an AppDelegate, a
Koin bootstrap, and a UIViewControllerRepresentable that hands off to
a Kotlin ComposeUIViewController. The SwiftUI starter under
/ios/ios/ui and /ios/ios/DecomposeHelpers is deleted.
Hammer.framework moves from :common to :composeUi so the framework
ships the Compose entry point; Xcode's Run Script invokes
:composeUi:embedAndSignAppleFrameworkForXcode. iosArm64 and
iosSimulatorArm64 targets are declared with libbacktrace source-info
so K/N crashes report file:line. 17 expects across composeUi get iOS
actuals (file pickers via filekit, image loading via Coil3,
LanguageUtil via NSLocale.preferredLanguages, etc).
The adaptive nav scaffolds — bottom bar at compact width, side nav
rail otherwise — are extracted from the Android activities into
shared ProjectSelectScaffold and ProjectRootScaffold in
composeUi/commonMain, along with their Modifier helpers and the
close-confirm dialogs. Android activities are now thin shells that
delegate to these; iOS calls the same scaffolds, so phone/tablet
layout is unified across all three platforms.
App icon and CFBundleDisplayName set so the home screen reads
"Hammer" with the brand logo instead of the blueprint placeholder.