also looking at this
fix(native): retain keyed controls and correct map and tab chrome
#368
32 files
+449
-100
| @@ -35,9 +35,9 @@ jobs: | ||
| 35 | 35 | fetch-depth: 0 |
| 36 | 36 | |
| 37 | 37 | - name: Setup Zig |
| 38 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 38 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 39 | 39 | with: |
| 40 | version: '0.11.93' | |
| 40 | version: '0.11.94' | |
| 41 | 41 | - name: Install Linux dependencies |
| 42 | 42 | run: | |
| 43 | 43 | sudo apt-get update |
| @@ -49,9 +49,9 @@ jobs: | ||
| 49 | 49 | fetch-depth: 0 |
| 50 | 50 | |
| 51 | 51 | - name: Setup Zig |
| 52 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 52 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 53 | 53 | with: |
| 54 | version: '0.11.93' | |
| 54 | version: '0.11.94' | |
| 55 | 55 | - name: Install Linux dependencies |
| 56 | 56 | run: | |
| 57 | 57 | sudo apt-get update |
| @@ -33,9 +33,9 @@ jobs: | ||
| 33 | 33 | steps: |
| 34 | 34 | - uses: actions/checkout@v6 |
| 35 | 35 | - name: Install the managed Android toolchain |
| 36 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 36 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 37 | 37 | with: |
| 38 | version: '0.11.93' | |
| 38 | version: '0.11.94' | |
| 39 | 39 | install: 'false' |
| 40 | 40 | packages: bun.com@1.4.3 git-scm.org@2.53.0 openjdk.org@17.0.20.8 android.com/cmdline-tools@14742923.0.0 |
| 41 | 41 | - name: Boot and dispose the stock Play image through the SDK |
| @@ -58,9 +58,9 @@ jobs: | ||
| 58 | 58 | steps: |
| 59 | 59 | - uses: actions/checkout@v6 |
| 60 | 60 | - name: Install the identical managed Android toolchain |
| 61 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 61 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 62 | 62 | with: |
| 63 | version: '0.11.93' | |
| 63 | version: '0.11.94' | |
| 64 | 64 | install: 'false' |
| 65 | 65 | packages: bun.com@1.4.3 git-scm.org@2.53.0 openjdk.org@17.0.20.8 android.com/cmdline-tools@14742923.0.0 |
| 66 | 66 | - name: Verify bounded budget validation and normal readiness contracts |
| @@ -93,9 +93,9 @@ jobs: | ||
| 93 | 93 | - uses: actions/checkout@v6 |
| 94 | 94 | |
| 95 | 95 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 96 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 96 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 97 | 97 | with: |
| 98 | version: '0.11.93' | |
| 98 | version: '0.11.94' | |
| 99 | 99 | |
| 100 | 100 | - name: Verify Zig installation |
| 101 | 101 | run: zig version |
| @@ -178,9 +178,9 @@ jobs: | ||
| 178 | 178 | - uses: actions/checkout@v6 |
| 179 | 179 | |
| 180 | 180 | - name: Setup Pantry (provides Bun and Zig) |
| 181 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 181 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 182 | 182 | with: |
| 183 | version: '0.11.93' | |
| 183 | version: '0.11.94' | |
| 184 | 184 | |
| 185 | 185 | - name: First-party Zig dependencies |
| 186 | 186 | uses: ./.github/actions/first-party-zig-deps |
| @@ -237,9 +237,9 @@ jobs: | ||
| 237 | 237 | - uses: actions/checkout@v6 |
| 238 | 238 | |
| 239 | 239 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 240 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 240 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 241 | 241 | with: |
| 242 | version: '0.11.93' | |
| 242 | version: '0.11.94' | |
| 243 | 243 | |
| 244 | 244 | - name: Install dependencies |
| 245 | 245 | run: bun install |
| @@ -283,9 +283,9 @@ jobs: | ||
| 283 | 283 | - uses: actions/checkout@v6 |
| 284 | 284 | |
| 285 | 285 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 286 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 286 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 287 | 287 | with: |
| 288 | version: '0.11.93' | |
| 288 | version: '0.11.94' | |
| 289 | 289 | |
| 290 | 290 | - name: Install dependencies |
| 291 | 291 | run: bun install |
| @@ -310,9 +310,9 @@ jobs: | ||
| 310 | 310 | - uses: actions/checkout@v6 |
| 311 | 311 | |
| 312 | 312 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 313 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 313 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 314 | 314 | with: |
| 315 | version: '0.11.93' | |
| 315 | version: '0.11.94' | |
| 316 | 316 | |
| 317 | 317 | - name: Install XcodeGen |
| 318 | 318 | run: brew install xcodegen |
| @@ -352,9 +352,9 @@ jobs: | ||
| 352 | 352 | - uses: actions/checkout@v6 |
| 353 | 353 | |
| 354 | 354 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 355 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 355 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 356 | 356 | with: |
| 357 | version: '0.11.93' | |
| 357 | version: '0.11.94' | |
| 358 | 358 | |
| 359 | 359 | - name: Setup Java |
| 360 | 360 | uses: actions/setup-java@v6 |
| @@ -403,9 +403,9 @@ jobs: | ||
| 403 | 403 | - uses: actions/checkout@v6 |
| 404 | 404 | |
| 405 | 405 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 406 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 406 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 407 | 407 | with: |
| 408 | version: '0.11.93' | |
| 408 | version: '0.11.94' | |
| 409 | 409 | |
| 410 | 410 | - name: Install dependencies |
| 411 | 411 | run: bun install |
| @@ -422,9 +422,9 @@ jobs: | ||
| 422 | 422 | - uses: actions/checkout@v6 |
| 423 | 423 | |
| 424 | 424 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 425 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 425 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 426 | 426 | with: |
| 427 | version: '0.11.93' | |
| 427 | version: '0.11.94' | |
| 428 | 428 | |
| 429 | 429 | - name: Install dependencies |
| 430 | 430 | run: bun install |
| @@ -449,9 +449,9 @@ jobs: | ||
| 449 | 449 | - uses: actions/checkout@v6 |
| 450 | 450 | |
| 451 | 451 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 452 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 452 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 453 | 453 | with: |
| 454 | version: '0.11.93' | |
| 454 | version: '0.11.94' | |
| 455 | 455 | |
| 456 | 456 | - name: Install dependencies |
| 457 | 457 | run: bun install |
| @@ -526,9 +526,9 @@ jobs: | ||
| 526 | 526 | - uses: actions/checkout@v6 |
| 527 | 527 | |
| 528 | 528 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 529 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 529 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 530 | 530 | with: |
| 531 | version: '0.11.93' | |
| 531 | version: '0.11.94' | |
| 532 | 532 | |
| 533 | 533 | - name: Install dependencies |
| 534 | 534 | run: bun install |
| @@ -584,9 +584,9 @@ jobs: | ||
| 584 | 584 | fetch-depth: 0 |
| 585 | 585 | |
| 586 | 586 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 587 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 587 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 588 | 588 | with: |
| 589 | version: '0.11.93' | |
| 589 | version: '0.11.94' | |
| 590 | 590 | |
| 591 | 591 | - name: Install Dependencies |
| 592 | 592 | run: bun install |
| @@ -76,7 +76,7 @@ on: | ||
| 76 | 76 | android_suite: |
| 77 | 77 | description: Android native fixture coverage for the android-navigation scope |
| 78 | 78 | type: choice |
| 79 | options: [focused, full, system-bars, maps, accessibility] | |
| 79 | options: [focused, full, system-bars, maps, accessibility, identity] | |
| 80 | 80 | default: focused |
| 81 | 81 | selectors: |
| 82 | 82 | description: Optional XCTest selector for focused native iOS rendering or navigation |
| @@ -113,9 +113,9 @@ jobs: | ||
| 113 | 113 | - uses: actions/checkout@v6 |
| 114 | 114 | |
| 115 | 115 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 116 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 116 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 117 | 117 | with: |
| 118 | version: '0.11.93' | |
| 118 | version: '0.11.94' | |
| 119 | 119 | |
| 120 | 120 | - name: Install dependencies |
| 121 | 121 | run: bun install --frozen-lockfile |
| @@ -167,7 +167,7 @@ jobs: | ||
| 167 | 167 | uses: actions/checkout@v6 |
| 168 | 168 | with: |
| 169 | 169 | repository: stacksjs/stx |
| 170 | ref: 3dd3240fc9a64f360e9690eee50c483be3325705 | |
| 170 | ref: 0d85afbab4b569ac2f09113a6c9a5d11d926145f | |
| 171 | 171 | path: .stx-compiler |
| 172 | 172 | |
| 173 | 173 | - name: Install stx compiler dependencies |
| @@ -267,9 +267,9 @@ jobs: | ||
| 267 | 267 | - uses: actions/checkout@v6 |
| 268 | 268 | |
| 269 | 269 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 270 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 270 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 271 | 271 | with: |
| 272 | version: '0.11.93' | |
| 272 | version: '0.11.94' | |
| 273 | 273 | |
| 274 | 274 | - name: Install XcodeGen |
| 275 | 275 | run: brew install xcodegen |
| @@ -286,7 +286,7 @@ jobs: | ||
| 286 | 286 | uses: actions/checkout@v6 |
| 287 | 287 | with: |
| 288 | 288 | repository: stacksjs/stx |
| 289 | ref: 3dd3240fc9a64f360e9690eee50c483be3325705 | |
| 289 | ref: 0d85afbab4b569ac2f09113a6c9a5d11d926145f | |
| 290 | 290 | path: .stx-compiler |
| 291 | 291 | |
| 292 | 292 | - name: Install stx compiler dependencies |
| @@ -400,9 +400,9 @@ jobs: | ||
| 400 | 400 | - uses: actions/checkout@v6 |
| 401 | 401 | |
| 402 | 402 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 403 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 403 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 404 | 404 | with: |
| 405 | version: '0.11.93' | |
| 405 | version: '0.11.94' | |
| 406 | 406 | |
| 407 | 407 | - name: Setup Java |
| 408 | 408 | uses: actions/setup-java@v6 |
| @@ -434,7 +434,7 @@ jobs: | ||
| 434 | 434 | uses: actions/checkout@v6 |
| 435 | 435 | with: |
| 436 | 436 | repository: stacksjs/stx |
| 437 | ref: 3dd3240fc9a64f360e9690eee50c483be3325705 | |
| 437 | ref: 0d85afbab4b569ac2f09113a6c9a5d11d926145f | |
| 438 | 438 | path: .stx-compiler |
| 439 | 439 | |
| 440 | 440 | - name: Install Android stx compiler dependencies |
| @@ -566,9 +566,9 @@ jobs: | ||
| 566 | 566 | - uses: actions/checkout@v6 |
| 567 | 567 | |
| 568 | 568 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 569 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 569 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 570 | 570 | with: |
| 571 | version: '0.11.93' | |
| 571 | version: '0.11.94' | |
| 572 | 572 | |
| 573 | 573 | - name: Cache Zig artifacts |
| 574 | 574 | uses: actions/cache@v5 |
| @@ -618,7 +618,7 @@ jobs: | ||
| 618 | 618 | api-level: ${{ fromJSON((inputs.scope != 'android-navigation' || inputs.android_suite == 'full') && '[34,35]' || inputs.android_suite == 'system-bars' && '[35]' || '[34]') }} |
| 619 | 619 | env: |
| 620 | 620 | CRAFT_ANDROID_NATIVE_SUITE: ${{ inputs.scope != 'android-navigation' && 'full' || inputs.android_suite || 'focused' }} |
| 621 | CRAFT_ANDROID_TEST_CLASS: ${{ inputs.scope == 'android-navigation' && inputs.android_suite == 'accessibility' && 'dev.craft.navigationtest.NativeNavigationTest#nativeTestIdsRemainQueryableWithoutReplacingVisibleLabels' || inputs.scope == 'android-navigation' && inputs.android_suite == 'maps' && 'dev.craft.navigationtest.NativeMapTest#keyFreeBasemapsKeepTheRecordedRouteAndDirectionSignsVisible,dev.craft.navigationtest.NativeMapTest#nativeMapMutationsUpdateThePinAndCameraWithoutReloadingTheStyle,dev.craft.navigationtest.NativeMapTest#showingTheLocationDotDoesNotRequestPermissionDuringRendering,dev.craft.navigationtest.NativeMapTest#controlledRegionEchoPreservesTheCameraAfterANativeDrag,dev.craft.navigationtest.NativeMapTest#backgroundAndResumeKeepTheNativeMapAndCamera' || inputs.scope == 'android-navigation' && inputs.android_suite == 'system-bars' && 'dev.craft.navigationtest.NativeNavigationTest#nativeScreenBackgroundOptionAppliesAndResets' || inputs.scope == 'android-navigation' && inputs.android_suite != 'full' && 'dev.craft.navigationtest.NativeNavigationTest#outgoingTabTapWinsOverSettlingSwipe,dev.craft.navigationtest.NativeNavigationTest#thirdTabTapWinsOverSettlingSwipe,dev.craft.navigationtest.NativeNavigationTest#cancelledSwipeTargetIsVisibleWhenSelectedLater,dev.craft.navigationtest.NativeNavigationTest#completedReverseSwipeKeepsRetainedTabVisible,dev.craft.navigationtest.NativeNavigationTest#nativePullToRefreshSurvivesViewReattachment,dev.craft.navigationtest.NativeNavigationTest#pushedScreenTabAccessoryOpensNativeRouteAndClearsOnBack,dev.craft.navigationtest.NativeFlatListTest#refreshAppearanceUsesTheScreenContrastFallback,dev.craft.navigationtest.NativeNavigationTest#nativeDatePickerKeepsFollowingControlsMounted' || '' }} | |
| 621 | CRAFT_ANDROID_TEST_CLASS: ${{ inputs.scope == 'android-navigation' && inputs.android_suite == 'accessibility' && 'dev.craft.navigationtest.NativeNavigationTest#nativeTestIdsRemainQueryableWithoutReplacingVisibleLabels' || inputs.scope == 'android-navigation' && inputs.android_suite == 'maps' && 'dev.craft.navigationtest.NativeMapTest#keyFreeBasemapsKeepTheRecordedRouteAndDirectionSignsVisible,dev.craft.navigationtest.NativeMapTest#nativeMapMutationsUpdateThePinAndCameraWithoutReloadingTheStyle,dev.craft.navigationtest.NativeMapTest#showingTheLocationDotDoesNotRequestPermissionDuringRendering,dev.craft.navigationtest.NativeMapTest#controlledRegionEchoPreservesTheCameraAfterANativeDrag,dev.craft.navigationtest.NativeMapTest#backgroundAndResumeKeepTheNativeMapAndCamera' || inputs.scope == 'android-navigation' && inputs.android_suite == 'identity' && 'dev.craft.navigationtest.NativeNavigationTest#repeatedTestIdsKeepDistinctProtocolViewsAcrossReorders' || inputs.scope == 'android-navigation' && inputs.android_suite == 'system-bars' && 'dev.craft.navigationtest.NativeNavigationTest#nativeScreenBackgroundOptionAppliesAndResets' || inputs.scope == 'android-navigation' && inputs.android_suite != 'full' && 'dev.craft.navigationtest.NativeNavigationTest#outgoingTabTapWinsOverSettlingSwipe,dev.craft.navigationtest.NativeNavigationTest#thirdTabTapWinsOverSettlingSwipe,dev.craft.navigationtest.NativeNavigationTest#cancelledSwipeTargetIsVisibleWhenSelectedLater,dev.craft.navigationtest.NativeNavigationTest#completedReverseSwipeKeepsRetainedTabVisible,dev.craft.navigationtest.NativeNavigationTest#nativePullToRefreshSurvivesViewReattachment,dev.craft.navigationtest.NativeNavigationTest#pushedScreenTabAccessoryOpensNativeRouteAndClearsOnBack,dev.craft.navigationtest.NativeFlatListTest#refreshAppearanceUsesTheScreenContrastFallback,dev.craft.navigationtest.NativeNavigationTest#nativeDatePickerKeepsFollowingControlsMounted' || '' }} | |
| 622 | 622 | if: ${{ inputs.scope != 'ios-navigation' && inputs.scope != 'ios-render' }} |
| 623 | 623 | runs-on: ubuntu-latest |
| 624 | 624 | timeout-minutes: 35 |
| @@ -642,7 +642,7 @@ jobs: | ||
| 642 | 642 | - uses: actions/checkout@v6 |
| 643 | 643 | with: |
| 644 | 644 | repository: stacksjs/stx |
| 645 | ref: 3dd3240fc9a64f360e9690eee50c483be3325705 | |
| 645 | ref: 0d85afbab4b569ac2f09113a6c9a5d11d926145f | |
| 646 | 646 | path: .stx-compiler |
| 647 | 647 | - run: bun install --no-save |
| 648 | 648 | working-directory: .stx-compiler |
| @@ -69,9 +69,9 @@ jobs: | ||
| 69 | 69 | fetch-depth: 0 |
| 70 | 70 | |
| 71 | 71 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 72 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 72 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 73 | 73 | with: |
| 74 | version: '0.11.93' | |
| 74 | version: '0.11.94' | |
| 75 | 75 | |
| 76 | 76 | - name: Install System Dependencies |
| 77 | 77 | if: matrix.platform.deps != '' |
| @@ -344,9 +344,9 @@ jobs: | ||
| 344 | 344 | # publish the Zig package here: a failed second leg would otherwise make |
| 345 | 345 | # an incomplete version visible in the pantry registry. |
| 346 | 346 | - name: Stage draft release |
| 347 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 347 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 348 | 348 | with: |
| 349 | version: '0.11.93' | |
| 349 | version: '0.11.94' | |
| 350 | 350 | install: 'false' |
| 351 | 351 | release: 'true' |
| 352 | 352 | release-draft: 'true' |
| @@ -435,9 +435,9 @@ jobs: | ||
| 435 | 435 | # paths; it does not rebuild a platform binary. The complete GitHub |
| 436 | 436 | # release is public before this registry version can become visible. |
| 437 | 437 | - name: Publish Zig package |
| 438 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 438 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 439 | 439 | with: |
| 440 | version: '0.11.93' | |
| 440 | version: '0.11.94' | |
| 441 | 441 | install: 'false' |
| 442 | 442 | publish: 'zig' |
| 443 | 443 | package-dir: packages/zig |
| @@ -644,9 +644,9 @@ jobs: | ||
| 644 | 644 | fetch-depth: 0 |
| 645 | 645 | |
| 646 | 646 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 647 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 647 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 648 | 648 | with: |
| 649 | version: '0.11.93' | |
| 649 | version: '0.11.94' | |
| 650 | 650 | |
| 651 | 651 | - name: Install Dependencies |
| 652 | 652 | run: bun install --frozen-lockfile |
| @@ -731,9 +731,9 @@ jobs: | ||
| 731 | 731 | fetch-depth: 0 |
| 732 | 732 | |
| 733 | 733 | - name: Setup Pantry (provides Bun, Zig, etc.) |
| 734 | uses: pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231 # v0.11.93 | |
| 734 | uses: pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4 # v0.11.94 | |
| 735 | 735 | with: |
| 736 | version: '0.11.93' | |
| 736 | version: '0.11.94' | |
| 737 | 737 | - uses: actions/setup-node@v6 |
| 738 | 738 | with: |
| 739 | 739 | node-version: '24' |
| @@ -61,9 +61,10 @@ capability APIs from the host. | ||
| 61 | 61 | The maintained Mobile E2E workflow checks out one exact stx compiler commit for |
| 62 | 62 | both simulator hosts. Update the iOS and Android refs together; the workflow |
| 63 | 63 | contract test rejects compiler skew between them. |
| 64 | The current pair is pinned to `3dd3240fc9a64f360e9690eee50c483be3325705`, | |
| 65 | which keeps a container's testID around a library component, draws `Checkbox` | |
| 66 | natively and keeps a style object's `transform` and `shadowOffset`. | |
| 64 | The current pair is pinned to `0d85afbab4b569ac2f09113a6c9a5d11d926145f`, | |
| 65 | which preserves authored reconciliation keys in created and cloned native nodes. | |
| 66 | This is the normally published STX `0.2.573` compiler. The native fixtures | |
| 67 | qualify its mutation metadata together with both native models. | |
| 67 | 68 | |
| 68 | 69 | The host-neutral layout contract lives in Craft's Zig runtime at |
| 69 | 70 | `packages/zig/src/native_layout.zig`. It defines the shared row/column flex |
| @@ -111,7 +111,8 @@ API 35 image took 943,520 ms in the retained diagnostic run | ||
| 111 | 111 | [39113277828](https://github.com/craft-native/craft/actions/runs/39113277828). |
| 112 | 112 | This is OS provisioning before app installation; app and performance budgets |
| 113 | 113 | are unchanged. Both modes require a readable event-log stream, SettingsProvider, |
| 114 | the package manager and System UI to be ready. Logs record the budget, actual | |
| 114 | the package manager and System UI to be ready, with HOME resolved beyond the | |
| 115 | automatic Android SDK setup activity. Logs record the budget, actual | |
| 115 | 116 | boot duration, host CPU interval, selected image, acceleration |
| 116 | 117 | probe, process identity and boot result. Disposal reaps only the owned process |
| 117 | 118 | and deletes only its disposable AVD; the managed SDK and other devices remain |
| @@ -153,8 +153,16 @@ export async function createAndroidEmulator(options: AndroidEmulatorOptions): Pr | ||
| 153 | 153 | // launching any app. boot_completed/System UI can precede logd's ADB |
| 154 | 154 | // event stream; declaring readiness then loses that first health check. |
| 155 | 155 | const events = await run([adb, '-s', serial, 'logcat', '-b', 'events', '-d', '-t', '1'], undefined, probeTimeout(), true) |
| 156 | // AOSP's first boot can report all those services ready while its | |
| 157 | // automatic SDK setup activity still owns HOME. Launching HOME then | |
| 158 | // opens that transient activity; after it finishes UI automation has | |
| 159 | // no foreground root. Wait for the real HOME destination instead of | |
| 160 | // modifying device setup or relaxing native acceptance. | |
| 161 | const home = await run([adb, '-s', serial, 'shell', 'cmd', 'package', 'resolve-activity', '--brief', '-a', 'android.intent.action.MAIN', '-c', 'android.intent.category.HOME'], undefined, probeTimeout(), true) | |
| 162 | const homeActivity = home.stdout.trim().split(/\r?\n/).at(-1) ?? '' | |
| 156 | 163 | booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 |
| 157 | 164 | && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim()) |
| 165 | && home.code === 0 && /^[\w.]+\/[\w.$]+$/.test(homeActivity) && !homeActivity.startsWith('com.android.sdksetup/') | |
| 158 | 166 | } |
| 159 | 167 | } |
| 160 | 168 | const checkedAt = Date.now() |
| @@ -558,7 +558,8 @@ describe('Craft Android builder', () => { | ||
| 558 | 558 | expect(nativeActivity).toContain('if (!current.contains(message))') |
| 559 | 559 | expect(nativeActivity).toContain('joinToString(", ")') |
| 560 | 560 | expect(nativeActivity).toContain('screen.controls.putAll(next)') |
| 561 | expect(nativeActivity).toContain('if (layout.getChildAt(index) !== childView)') | |
| 561 | expect(nativeActivity).toContain('if (childView.parent !== layout)') | |
| 562 | expect(nativeActivity).toContain('desired.forEach(layout::bringChildToFront)') | |
| 562 | 563 | expect(nativeActivity).toContain('val modal = screen.modalVisibility.entries.lastOrNull') |
| 563 | 564 | expect(nativeActivity).toContain('val suppressDismiss = !visible && consumeDismissSuppression()') |
| 564 | 565 | expect(nativeActivity).toContain('(modal.key as? CraftNativeModalView)?.requestClose()') |
| @@ -30,7 +30,8 @@ class CraftNativeMutationDocument { | ||
| 30 | 30 | var events: JSONObject, |
| 31 | 31 | var text: MutableList<String>, |
| 32 | 32 | var children: MutableList<String>, |
| 33 | var parent: String? | |
| 33 | var parent: String?, | |
| 34 | val key: String? = null | |
| 34 | 35 | ) { |
| 35 | 36 | fun duplicate(): Node = copy( |
| 36 | 37 | props = JSONObject(props.toString()), |
| @@ -175,7 +176,8 @@ class CraftNativeMutationDocument { | ||
| 175 | 176 | dictionary(value.opt("events")), |
| 176 | 177 | textChildren(if (value.has("children")) value.opt("children") else value.opt("text")), |
| 177 | 178 | mutableListOf(), |
| 178 | null | |
| 179 | null, | |
| 180 | key = value.opt("key") as? String | |
| 179 | 181 | ) |
| 180 | 182 | return if (makeRoot) id else root |
| 181 | 183 | } |
| @@ -299,6 +301,7 @@ class CraftNativeMutationDocument { | ||
| 299 | 301 | .put("style", node.style) |
| 300 | 302 | .put("events", node.events) |
| 301 | 303 | .put("children", children) |
| 304 | .also { value -> node.key?.let { value.put("key", it) } } | |
| 302 | 305 | } |
| 303 | 306 | |
| 304 | 307 | private fun importNode( |
| @@ -322,7 +325,8 @@ class CraftNativeMutationDocument { | ||
| 322 | 325 | dictionary(value.opt("events")), |
| 323 | 326 | mutableListOf(), |
| 324 | 327 | mutableListOf(), |
| 325 | parent | |
| 328 | parent, | |
| 329 | key = value.opt("key") as? String | |
| 326 | 330 | ) |
| 327 | 331 | nodes[id] = node |
| 328 | 332 | for (index in 0 until children.length()) { |
| @@ -4937,8 +4937,11 @@ class MainActivity : AppCompatActivity() { | ||
| 4937 | 4937 | val explicitKey = authoredKey(node, props) |
| 4938 | 4938 | // Descendants are scoped to this retained identity, not its sibling |
| 4939 | 4939 | // position. Moving a keyed parent must retain its nested inputs too. |
| 4940 | // A test selector may intentionally identify several list rows. The | |
| 4941 | // compiler's unique protocol ID must win over that legacy fallback. | |
| 4940 | 4942 | val identity = explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" } |
| 4941 | 4943 | ?: protocolId?.let { "id:$it" } |
| 4944 | ?: props.optString("testID").takeIf { it.isNotBlank() }?.let { "${path.substringBeforeLast('.', "")}/$it" } | |
| 4942 | 4945 | ?: path |
| 4943 | 4946 | val previous = previousControls[identity] |
| 4944 | 4947 | val reusableInput = previous as? EditText |
| @@ -5824,7 +5827,7 @@ class MainActivity : AppCompatActivity() { | ||
| 5824 | 5827 | } |
| 5825 | 5828 | |
| 5826 | 5829 | private fun authoredKey(node: JSONObject, props: JSONObject): String? { |
| 5827 | val direct = listOf(node.optString("key"), props.optString("key"), props.optString("testID")) | |
| 5830 | val direct = listOf(node.optString("key"), props.optString("key")) | |
| 5828 | 5831 | .firstOrNull { it.isNotBlank() } |
| 5829 | 5832 | if (direct != null) return direct |
| 5830 | 5833 | return null |
| @@ -5880,17 +5883,23 @@ class MainActivity : AppCompatActivity() { | ||
| 5880 | 5883 | } |
| 5881 | 5884 | if (craftNativeOptionalString(style.opt("flexDirection"))?.endsWith("-reverse") == true) desired.reverse() |
| 5882 | 5885 | val desiredSet = desired.toSet() |
| 5883 | for ((index, childView) in desired.withIndex()) { | |
| 5884 | if (layout.getChildAt(index) !== childView) { | |
| 5885 | if (index < layout.childCount && layout.getChildAt(index) !in desiredSet) layout.removeViewAt(index) | |
| 5886 | for (index in layout.childCount - 1 downTo 0) { | |
| 5887 | if (layout.getChildAt(index) !in desiredSet) layout.removeViewAt(index) | |
| 5888 | } | |
| 5889 | for (childView in desired) { | |
| 5890 | if (childView.parent !== layout) { | |
| 5886 | 5891 | (childView.parent as? ViewGroup)?.removeView(childView) |
| 5887 | layout.addView(childView, index) | |
| 5892 | layout.addView(childView) | |
| 5888 | 5893 | } |
| 5889 | 5894 | val params = childView.layoutParams as? LinearLayout.LayoutParams |
| 5890 | 5895 | ?: LinearLayout.LayoutParams(childView.layoutParams?.width ?: -1, childView.layoutParams?.height ?: -2) |
| 5891 | 5896 | childView.layoutParams = params |
| 5892 | 5897 | } |
| 5893 | while (layout.childCount > desired.size) layout.removeViewAt(layout.childCount - 1) | |
| 5898 | // Reorder attached children without removing their focused inputs. | |
| 5899 | // Removing and adding an existing row clears Android's native focus. | |
| 5900 | if (desired.indices.any { layout.getChildAt(it) !== desired[it] }) { | |
| 5901 | desired.forEach(layout::bringChildToFront) | |
| 5902 | } | |
| 5894 | 5903 | layout.requestLayout() |
| 5895 | 5904 | } |
| 5896 | 5905 | |
| @@ -6573,11 +6582,11 @@ class MainActivity : AppCompatActivity() { | ||
| 6573 | 6582 | } |
| 6574 | 6583 | |
| 6575 | 6584 | private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) { |
| 6576 | view.tag = fallbackLabel | |
| 6585 | val testId = craftNativeOptionalString(props.opt("testID")) | |
| 6586 | view.tag = testId?.takeIf { it.isNotBlank() } ?: fallbackLabel | |
| 6577 | 6587 | val label = craftNativeOptionalString(props.opt("accessibilityLabel")) |
| 6578 | 6588 | ?: craftNativeOptionalString(props.opt("alt"))?.takeIf { type == "Image" || type == "ImageBackground" } |
| 6579 | 6589 | val value = craftNativeOptionalString(props.opt("accessibilityValue")) |
| 6580 | val testId = craftNativeOptionalString(props.opt("testID")) | |
| 6581 | 6590 | view.contentDescription = when { |
| 6582 | 6591 | label != null && value != null -> "$label, $value" |
| 6583 | 6592 | label != null -> label |
| @@ -822,6 +822,28 @@ final class NativeRenderUnitTests: XCTestCase { | ||
| 822 | 822 | XCTAssertTrue(scroll === find(UIScrollView.self, key: "scroll", below: controller.view)) |
| 823 | 823 | } |
| 824 | 824 | |
| 825 | func testMutationPreservesAuthoredKeysAcrossImportCreationUpdatesAndMoves() throws { | |
| 826 | let document = CraftNativeMutationDocument() | |
| 827 | document.replace(with: [ | |
| 828 | "id": "root", "type": "View", "key": "root-key", "children": [ | |
| 829 | ["id": "existing", "type": "TextInput", "key": "draft-key", "props": ["value": "draft"]], | |
| 830 | ], | |
| 831 | ]) | |
| 832 | XCTAssertEqual(document.node("existing")?["key"] as? String, "draft-key") | |
| 833 | let result = try document.apply(batch(1, [ | |
| 834 | ["op": "createNode", "id": "created", "node": ["type": "View", "key": "created-key"]], | |
| 835 | ["op": "insertChild", "parentId": "root", "childId": "created", "index": 1], | |
| 836 | ["op": "updateNode", "id": "existing", "patch": ["props": ["value": "edited"]]], | |
| 837 | ["op": "moveChild", "parentId": "root", "childId": "created", "index": 0], | |
| 838 | ])) | |
| 839 | let rendered = try XCTUnwrap(result.document) | |
| 840 | let children = try XCTUnwrap(rendered["children"] as? [[String: Any]]) | |
| 841 | XCTAssertEqual(rendered["key"] as? String, "root-key") | |
| 842 | XCTAssertEqual(children[0]["key"] as? String, "created-key") | |
| 843 | XCTAssertEqual(children[1]["key"] as? String, "draft-key") | |
| 844 | XCTAssertEqual((document.node("existing")?["props"] as? [String: Any])?["value"] as? String, "edited") | |
| 845 | } | |
| 846 | ||
| 825 | 847 | func testEveryContainerTheRendererDrawsTakesChildrenAndALeafDoesNot() throws { |
| 826 | 848 | for type in CraftNativeMutationDocument.containerTypes.sorted() + ["svg", "pressable"] { |
| 827 | 849 | let document = CraftNativeMutationDocument() |
| @@ -901,6 +923,44 @@ final class NativeRenderUnitTests: XCTestCase { | ||
| 901 | 923 | XCTAssertEqual(marker.transform.b, 1, accuracy: 0.02) |
| 902 | 924 | } |
| 903 | 925 | |
| 926 | func testMapDirectionSignsKeepALightBackdropAndTheAuthoredColor() throws { | |
| 927 | for appearance in [UIUserInterfaceStyle.light, .dark] { | |
| 928 | let controller = CraftNativeScreenController(config: CraftConfig()) | |
| 929 | controller.overrideUserInterfaceStyle = appearance | |
| 930 | controller.loadViewIfNeeded() | |
| 931 | controller.render([ | |
| 932 | "type": "Map", "props": ["key": "contrast-map"], | |
| 933 | "style": ["width": 320, "height": 320], | |
| 934 | "children": [["type": "MapMarker", "props": ["id": "direction", "geometry": [37.7, -119.5], "bearing": 90, "pinColor": "#365b2d"]]], | |
| 935 | ]) | |
| 936 | let map = try XCTUnwrap(find(MKMapView.self, key: "contrast-map", below: controller.view)) | |
| 937 | let annotation = try XCTUnwrap(map.annotations.first) | |
| 938 | let marker = try XCTUnwrap(map.delegate?.mapView?(map, viewFor: annotation)) | |
| 939 | let image = try XCTUnwrap(marker.image) | |
| 940 | let raster = UIGraphicsImageRenderer(size: image.size).image { _ in image.draw(at: .zero) } | |
| 941 | let cgImage = try XCTUnwrap(raster.cgImage) | |
| 942 | var pixels = [UInt8](repeating: 0, count: cgImage.width * cgImage.height * 4) | |
| 943 | try pixels.withUnsafeMutableBytes { bytes in | |
| 944 | let context = try XCTUnwrap(CGContext(data: bytes.baseAddress, width: cgImage.width, height: cgImage.height, | |
| 945 | bitsPerComponent: 8, bytesPerRow: cgImage.width * 4, space: CGColorSpaceCreateDeviceRGB(), | |
| 946 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)) | |
| 947 | context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height)) | |
| 948 | } | |
| 949 | var opaque = 0 | |
| 950 | var light = 0 | |
| 951 | var authored = 0 | |
| 952 | for offset in stride(from: 0, to: pixels.count, by: 4) where pixels[offset + 3] > 240 { | |
| 953 | opaque += 1 | |
| 954 | let red = Int(pixels[offset]), green = Int(pixels[offset + 1]), blue = Int(pixels[offset + 2]) | |
| 955 | if red > 235 && green > 235 && blue > 235 { light += 1 } | |
| 956 | if abs(red - 54) < 12 && abs(green - 91) < 12 && abs(blue - 45) < 12 { authored += 1 } | |
| 957 | } | |
| 958 | XCTAssertGreaterThan(opaque, 50) | |
| 959 | XCTAssertGreaterThan(light, opaque / 3, "direction signs need an opaque light backdrop over the basemap") | |
| 960 | XCTAssertGreaterThan(authored, 10, "the native direction sign ignored its authored pinColor") | |
| 961 | } | |
| 962 | } | |
| 963 | ||
| 904 | 964 | func testMapPreservesAuthoredPolylineOrderAcrossUpdatesAndMoves() throws { |
| 905 | 965 | let controller = CraftNativeScreenController(config: CraftConfig()) |
| 906 | 966 | let window = try hostWindow() |
| @@ -12,7 +12,16 @@ struct HostApp: App { | ||
| 12 | 12 | Text(taps.isEmpty ? "none" : taps.joined(separator: ",")) |
| 13 | 13 | .lineLimit(1) |
| 14 | 14 | .accessibilityIdentifier("received-taps") |
| 15 | Spacer() | |
| 15 | ScrollView { | |
| 16 | VStack { | |
| 17 | Button("Start of scroll fixture") {} | |
| 18 | .accessibilityIdentifier("driver-scroll-start") | |
| 19 | Color.clear.frame(height: 1600).accessibilityHidden(true) | |
| 20 | Button("End of scroll fixture") {} | |
| 21 | .accessibilityIdentifier("driver-scroll-end") | |
| 22 | } | |
| 23 | .frame(maxWidth: .infinity) | |
| 24 | } | |
| 16 | 25 | HStack { |
| 17 | 26 | ForEach(names, id: \.self) { name in |
| 18 | 27 | Button(name) { taps.append(name) } |
| @@ -259,6 +259,10 @@ try { | ||
| 259 | 259 | ]) }, |
| 260 | 260 | { command: 'labelid', id: 'received-taps', text: inputSequence.join(',') }, |
| 261 | 261 | { command: 'selected', text: 'Four' }, |
| 262 | { command: 'scrollto', text: 'End of scroll fixture' }, | |
| 263 | { command: 'expectid', text: 'driver-scroll-end' }, | |
| 264 | { command: 'scrollto', text: 'Start of scroll fixture' }, | |
| 265 | { command: 'expectid', text: 'driver-scroll-start' }, | |
| 262 | 266 | ]) |
| 263 | 267 | initSimulatorDriver({ output: driverOutput, bundleId: 'dev.craft.timedinput.host', |
| 264 | 268 | bundleIdPrefix: 'dev.craft.timedinput', projectName: 'TimedInputDriver', tabs: inputNames, script }) |
| @@ -1709,7 +1709,7 @@ describe('Craft iOS builder', () => { | ||
| 1709 | 1709 | // A screen can ask the tab bar away while it shows; the next one says again. |
| 1710 | 1710 | expect(screen).toContain('if let hidden = options["tabBarHidden"] as? Bool {') |
| 1711 | 1711 | expect(screen).toContain('CraftChrome.shared.setHiddenByScreen(tabBarHidden)') |
| 1712 | expect(screen).toContain('CraftChrome.shared.updateTabAccessory(nil)') | |
| 1712 | expect(screen).toContain('CraftChrome.shared.clearTabAccessory(ownedBy: self)') | |
| 1713 | 1713 | expect(screen).toContain('if let contentSwipeBack = options["contentSwipeBack"] as? Bool {') |
| 1714 | 1714 | expect(screen).toContain('private(set) var contentSwipeBack = true') |
| 1715 | 1715 | expect(screen).toContain('if let tabSwipeEnabled = options["tabSwipeEnabled"] as? Bool {') |
| @@ -670,6 +670,7 @@ final class CraftChrome: ObservableObject { | ||
| 670 | 670 | |
| 671 | 671 | @Published var tabs: [Tab] = [] |
| 672 | 672 | @Published var tabAccessory: Tab? |
| 673 | private weak var tabAccessoryOwner: AnyObject? | |
| 673 | 674 | @Published var selected: String? |
| 674 | 675 | let tabSwipeProgress = CraftTabSwipeProgress() |
| 675 | 676 | private(set) var tabSwipePosition: CGFloat? { |
| @@ -771,7 +772,7 @@ final class CraftChrome: ObservableObject { | ||
| 771 | 772 | tabs[index] = next |
| 772 | 773 | } |
| 773 | 774 | /// A destination beside the tab pill, outside the tab stacks and swipe order. |
| 774 | func updateTabAccessory(_ item: [String: Any]?) { | |
| 775 | func updateTabAccessory(_ item: [String: Any]?, owner: AnyObject? = nil) { | |
| 775 | 776 | let next: Tab? |
| 776 | 777 | if let item, let path = item["path"] as? String, |
| 777 | 778 | path.hasPrefix("/"), !path.hasPrefix("//"), |
| @@ -782,10 +783,17 @@ final class CraftChrome: ObservableObject { | ||
| 782 | 783 | } else { |
| 783 | 784 | next = nil |
| 784 | 785 | } |
| 786 | tabAccessoryOwner = owner | |
| 785 | 787 | guard tabAccessory != next else { return } |
| 786 | 788 | tabAccessory = next |
| 787 | 789 | } |
| 788 | 790 | |
| 791 | /// A retained or prepared screen cannot clear another screen's chrome. | |
| 792 | func clearTabAccessory(ownedBy owner: AnyObject) { | |
| 793 | guard tabAccessoryOwner === owner else { return } | |
| 794 | updateTabAccessory(nil) | |
| 795 | } | |
| 796 | ||
| 789 | 797 | func setHiddenByScreen(_ hidden: Bool) { |
| 790 | 798 | guard hiddenByScreen != hidden else { return } |
| 791 | 799 | hiddenByScreen = hidden |
| @@ -29,6 +29,7 @@ struct CraftNativeMutationResult { | ||
| 29 | 29 | final class CraftNativeMutationDocument { |
| 30 | 30 | private struct Node { |
| 31 | 31 | let id: String |
| 32 | let key: String? | |
| 32 | 33 | var type: String |
| 33 | 34 | var props: [String: Any] |
| 34 | 35 | var style: [String: Any] |
| @@ -177,6 +178,7 @@ final class CraftNativeMutationDocument { | ||
| 177 | 178 | } |
| 178 | 179 | nodes[id] = Node( |
| 179 | 180 | id: id, |
| 181 | key: value["key"] as? String, | |
| 180 | 182 | type: type, |
| 181 | 183 | props: dictionary(value["props"]), |
| 182 | 184 | style: dictionary(value["style"]), |
| @@ -330,6 +332,7 @@ final class CraftNativeMutationDocument { | ||
| 330 | 332 | "style": node.style, |
| 331 | 333 | "events": node.events, |
| 332 | 334 | ] |
| 335 | if let key = node.key { value["key"] = key } | |
| 333 | 336 | let text: [Any] = node.text |
| 334 | 337 | let children: [Any] = node.children.compactMap { materialize($0, nodes: nodes) } |
| 335 | 338 | value["children"] = text + children |
| @@ -350,6 +353,7 @@ final class CraftNativeMutationDocument { | ||
| 350 | 353 | let children = value["children"] as? [Any] ?? [] |
| 351 | 354 | nodes[id] = Node( |
| 352 | 355 | id: id, |
| 356 | key: value["key"] as? String, | |
| 353 | 357 | type: value["type"] as? String ?? "View", |
| 354 | 358 | props: props, |
| 355 | 359 | style: dictionary(value["style"]), |
| @@ -1435,21 +1435,47 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate { | ||
| 1435 | 1435 | |
| 1436 | 1436 | private func configureMarkerView(_ view: MKAnnotationView, id: String) { |
| 1437 | 1437 | if let bearing = markerBearings[id] { |
| 1438 | let configuration = UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold) | |
| 1439 | .applying(UIImage.SymbolConfiguration(paletteColors: [.white, .systemGreen])) | |
| 1440 | // MapKit otherwise tints this as a monochrome template and loses | |
| 1441 | // the contrasting route arrow when displayed over map content. | |
| 1442 | view.image = UIImage(systemName: "arrow.up.circle.fill", withConfiguration: configuration)? | |
| 1443 | .withRenderingMode(.alwaysOriginal) | |
| 1438 | let color = (markerPinColors[id] ?? .systemGreen).resolvedColor(with: traitCollection) | |
| 1439 | view.tintColor = color | |
| 1440 | view.image = Self.markerImage(color: color, direction: true) | |
| 1444 | 1441 | view.transform = CGAffineTransform(rotationAngle: CGFloat((bearing - camera.heading) * .pi / 180)) |
| 1445 | 1442 | } else { |
| 1446 | view.tintColor = markerPinColors[id] ?? .systemRed | |
| 1447 | view.image = UIImage(systemName: "mappin.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28))? | |
| 1448 | .withTintColor(view.tintColor, renderingMode: .alwaysOriginal) | |
| 1443 | let color = (markerPinColors[id] ?? .systemRed).resolvedColor(with: traitCollection) | |
| 1444 | view.tintColor = color | |
| 1445 | view.image = Self.markerImage(color: color, direction: false) | |
| 1449 | 1446 | view.transform = .identity |
| 1450 | 1447 | } |
| 1451 | 1448 | } |
| 1452 | 1449 | |
| 1450 | private static let markerImages: NSCache<NSString, UIImage> = { | |
| 1451 | let cache = NSCache<NSString, UIImage>() | |
| 1452 | cache.countLimit = 64 | |
| 1453 | return cache | |
| 1454 | }() | |
| 1455 | ||
| 1456 | private static func markerImage(color: UIColor, direction: Bool) -> UIImage { | |
| 1457 | let key = "\(direction ? "direction" : "pin"):\(color.description)" as NSString | |
| 1458 | if let cached = markerImages.object(forKey: key) { return cached } | |
| 1459 | let size = direction ? CGSize(width: 20, height: 24) : CGSize(width: 28, height: 28) | |
| 1460 | // Explicit native drawing preserves the light backing independently | |
| 1461 | // of MapKit's tint and SF Symbols' multicolor rendering preferences. | |
| 1462 | let image = UIGraphicsImageRenderer(size: size).image { _ in | |
| 1463 | let border = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size).insetBy(dx: 1, dy: 1), | |
| 1464 | cornerRadius: (size.width - 2) / 2) | |
| 1465 | UIColor.white.setFill() | |
| 1466 | border.fill() | |
| 1467 | color.setStroke() | |
| 1468 | border.lineWidth = 1.5 | |
| 1469 | border.stroke() | |
| 1470 | let symbol = UIImage(systemName: direction ? "arrow.up" : "mappin", | |
| 1471 | withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold))? | |
| 1472 | .withTintColor(color, renderingMode: .alwaysOriginal) | |
| 1473 | symbol?.draw(in: direction ? CGRect(x: 5, y: 6, width: 10, height: 12) : CGRect(x: 7, y: 5, width: 14, height: 18)) | |
| 1474 | }.withRenderingMode(.alwaysOriginal) | |
| 1475 | markerImages.setObject(image, forKey: key) | |
| 1476 | return image | |
| 1477 | } | |
| 1478 | ||
| 1453 | 1479 | override func layoutSubviews() { |
| 1454 | 1480 | super.layoutSubviews() |
| 1455 | 1481 | if tileTemplate != nil { |
| @@ -4621,10 +4647,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, | ||
| 4621 | 4647 | super.viewWillDisappear(animated) |
| 4622 | 4648 | screenVisibilityChanged(false) |
| 4623 | 4649 | stopCelebrations(in: view) |
| 4624 | // The accessory belongs to the visible native screen. Clear the | |
| 4625 | // shared shell state before a hybrid/web route appears, otherwise a | |
| 4626 | // destination from the outgoing screen remains tappable on top of it. | |
| 4627 | CraftChrome.shared.updateTabAccessory(nil) | |
| 4650 | // Release only this screen's accessory. A retained or prepared screen | |
| 4651 | // can disappear after the destination has already published its own. | |
| 4652 | CraftChrome.shared.clearTabAccessory(ownedBy: self) | |
| 4628 | 4653 | // Popped (or its stack dismissed) while the bar was away: it comes |
| 4629 | 4654 | // back for whatever shows next, a web page included. |
| 4630 | 4655 | if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true { |
| @@ -4669,7 +4694,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, | ||
| 4669 | 4694 | private func restoreShellChrome() { |
| 4670 | 4695 | guard !isAppScript else { return } |
| 4671 | 4696 | CraftChrome.shared.setHiddenByScreen(tabBarHidden) |
| 4672 | CraftChrome.shared.updateTabAccessory(tabAccessory) | |
| 4697 | CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self) | |
| 4673 | 4698 | } |
| 4674 | 4699 | |
| 4675 | 4700 | /// The screen came on screen or went off it: a screen pushed over it was |
| @@ -5340,7 +5365,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, | ||
| 5340 | 5365 | if options.keys.contains("tabAccessory") { |
| 5341 | 5366 | tabAccessory = options["tabAccessory"] as? [String: Any] |
| 5342 | 5367 | if navigationController?.topViewController === self, view.window != nil { |
| 5343 | CraftChrome.shared.updateTabAccessory(tabAccessory) | |
| 5368 | CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self) | |
| 5344 | 5369 | } |
| 5345 | 5370 | } |
| 5346 | 5371 | if options.keys.contains("title") { |
| @@ -6865,7 +6890,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, | ||
| 6865 | 6890 | |
| 6866 | 6891 | private func explicitKey(_ node: [String: Any], props: [String: Any]) -> String? { |
| 6867 | 6892 | if let authored = authoredKey(node, props: props) { return authored } |
| 6868 | return (node["id"] as? String).flatMap { $0.isEmpty ? nil : $0 } | |
| 6893 | // Repeated test selectors are valid for list rows. Retain each row by | |
| 6894 | // its unique protocol ID before considering legacy selector identity. | |
| 6895 | return [node["id"], props["testID"]] | |
| 6896 | .compactMap { $0 as? String } | |
| 6897 | .first { !$0.isEmpty } | |
| 6869 | 6898 | } |
| 6870 | 6899 | |
| 6871 | 6900 | /// The key a node's author gave it. A wrapper of one child (a |
| @@ -6877,7 +6906,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, | ||
| 6877 | 6906 | private func authoredKey(_ node: [String: Any], props: [String: Any]) -> String? { |
| 6878 | 6907 | // The current @stacksjs/stx native compiler emits key in props; accept the IR |
| 6879 | 6908 | // field too so keyed children keep working when the compiler adopts it. |
| 6880 | let direct = [node["key"], props["key"], props["testID"]] | |
| 6909 | let direct = [node["key"], props["key"]] | |
| 6881 | 6910 | .compactMap { $0 as? String } |
| 6882 | 6911 | .first { !$0.isEmpty } |
| 6883 | 6912 | if let direct { return direct } |
| @@ -448,7 +448,11 @@ final class DriverTests: XCTestCase { | ||
| 448 | 448 | var found = element(containing: arg) |
| 449 | 449 | var swipes = 0 |
| 450 | 450 | while (found == nil || !(found!.isHittable)) && swipes < 8 { |
| 451 | app.swipeUp() | |
| 451 | if let target = found, !target.frame.isEmpty, target.frame.midY < app.frame.midY { | |
| 452 | app.swipeDown() | |
| 453 | } else { | |
| 454 | app.swipeUp() | |
| 455 | } | |
| 452 | 456 | swipes += 1 |
| 453 | 457 | found = element(containing: arg) |
| 454 | 458 | } |
| @@ -5,7 +5,7 @@ import { join } from 'node:path' | ||
| 5 | 5 | import { createAndroidEmulator } from '../../../android/src/emulator' |
| 6 | 6 | import type { AndroidEmulatorOptions } from '../../../android/src/emulator' |
| 7 | 7 | |
| 8 | function fixture(hardware: boolean, delayedSettings = false, delayedEvents = false): { root: string, options: AndroidEmulatorOptions } { | |
| 8 | function fixture(hardware: boolean, delayedSettings = false, delayedEvents = false, delayedHome = false): { root: string, options: AndroidEmulatorOptions } { | |
| 9 | 9 | const root = mkdtempSync(join(tmpdir(), 'craft emulator fixture ')) |
| 10 | 10 | const sdk = join(root, 'managed sdk') |
| 11 | 11 | for (const directory of ['bin', 'platform-tools', 'emulator']) mkdirSync(join(sdk, directory), { recursive: true }) |
| @@ -30,6 +30,13 @@ else | ||
| 30 | 30 | printf '1\\n' ;; |
| 31 | 31 | *'pm path android') printf 'package:/system/framework/framework-res.apk\\n' ;; |
| 32 | 32 | *'pidof com.android.systemui') printf '1234\\n' ;; |
| 33 | *'cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.HOME') | |
| 34 | if [ '${delayedHome ? 'yes' : 'no'}' = yes ] && [ ! -f "$ANDROID_HOME/home-ready" ]; then | |
| 35 | touch "$ANDROID_HOME/home-ready" | |
| 36 | printf 'com.android.sdksetup/.DefaultActivity\\n' | |
| 37 | else | |
| 38 | printf 'com.android.launcher3/.Launcher\\n' | |
| 39 | fi ;; | |
| 33 | 40 | *'logcat -b events -d -t 1') |
| 34 | 41 | if [ '${delayedEvents ? 'yes' : 'no'}' = yes ] && [ ! -f "$ANDROID_HOME/events-ready" ]; then |
| 35 | 42 | touch "$ANDROID_HOME/events-ready" |
| @@ -113,6 +120,21 @@ test('boot and System UI do not admit a device before its event log is readable' | ||
| 113 | 120 | finally { await session?.dispose(); rmSync(root, { recursive: true, force: true }) } |
| 114 | 121 | }, 10_000) |
| 115 | 122 | |
| 123 | test('boot completion waits for the automatic SDK setup activity to release HOME', async () => { | |
| 124 | const { root, options } = fixture(true, false, false, true) | |
| 125 | let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined | |
| 126 | try { | |
| 127 | session = await createAndroidEmulator(options) | |
| 128 | const trace = readFileSync(join(options.artifactsDirectory, 'device-commands.jsonl'), 'utf8').trim().split('\n').map(line => JSON.parse(line)) | |
| 129 | const home = trace.filter(entry => entry.args.includes('resolve-activity')) | |
| 130 | expect(home.map(entry => entry.stdout.trim())).toEqual(['com.android.sdksetup/.DefaultActivity', 'com.android.launcher3/.Launcher']) | |
| 131 | const identity = JSON.parse(readFileSync(join(options.artifactsDirectory, 'android-emulator.json'), 'utf8')) | |
| 132 | expect(Date.parse(home[1].finishedAt)).toBeLessThanOrEqual(Date.parse(identity.bootedAt)) | |
| 133 | expect(identity.bootTimeoutMs).toBe(600_000) | |
| 134 | } | |
| 135 | finally { await session?.dispose(); rmSync(root, { recursive: true, force: true }) } | |
| 136 | }, 10_000) | |
| 137 | ||
| 116 | 138 | test('the SDK process fixture records hardware mode and reaps only its owned process', async () => { |
| 117 | 139 | const { root, options } = fixture(true) |
| 118 | 140 | options.bootTimeoutMs = 42_000 |
| @@ -2924,7 +2924,7 @@ describe('native renderer component contract', () => { | ||
| 2924 | 2924 | expect(iosNavigationTest).toContain('craft-tab-accessory') |
| 2925 | 2925 | expect(androidNavigationTest).toContain('pushedScreenTabAccessoryOpensNativeRouteAndClearsOnBack') |
| 2926 | 2926 | expect(androidNavigationTest).toContain('Open capabilities') |
| 2927 | expect(ios).toContain('CraftChrome.shared.updateTabAccessory(nil)') | |
| 2927 | expect(ios).toContain('CraftChrome.shared.clearTabAccessory(ownedBy: self)') | |
| 2928 | 2928 | expect(androidShell).toContain('accessorySpec?.optString("path")?.let { onAccessory?.invoke(it) }') |
| 2929 | 2929 | expect(guide).toContain('`tabAccessory` adds one destination button beside the tab pill') |
| 2930 | 2930 | expect(iosReadme).toContain('`tabAccessory` destination beside the tab pill') |
| @@ -3003,7 +3003,8 @@ describe('native renderer component contract', () => { | ||
| 3003 | 3003 | }) |
| 3004 | 3004 | |
| 3005 | 3005 | it('prefers authored keys over compiler ids when reconciling native rows', () => { |
| 3006 | expect(ios).toContain('let direct = [node["key"], props["key"], props["testID"]]') | |
| 3006 | expect(ios).toContain('let direct = [node["key"], props["key"]]') | |
| 3007 | expect(ios).toContain('[node["id"], props["testID"]]') | |
| 3007 | 3008 | // Through a lone wrapped child (a component's wrapper) only. |
| 3008 | 3009 | expect(ios).toContain('return authoredKey(only, props: only["props"] as? [String: Any] ?? [:])') |
| 3009 | 3010 | expect(iosFlatList).toContain('[node["key"], props["key"], props["testID"], node["id"]]') |
| @@ -18,7 +18,7 @@ type Job = { | ||
| 18 | 18 | } |
| 19 | 19 | const release = Bun.YAML.parse(readFileSync(join(import.meta.dir, '../.github/workflows/release.yml'), 'utf8')) as { jobs: Record<string, Job> } |
| 20 | 20 | const artifactDownload = 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' |
| 21 | const stxCompilerCommit = '3dd3240fc9a64f360e9690eee50c483be3325705' | |
| 21 | const stxCompilerCommit = '0d85afbab4b569ac2f09113a6c9a5d11d926145f' | |
| 22 | 22 | const needs = (job: Job) => typeof job.needs === 'string' ? [job.needs] : job.needs ?? [] |
| 23 | 23 | const steps = (job: Job) => job.steps?.map(step => step.run ?? '').join('\n') ?? '' |
| 24 | 24 | |
| @@ -101,8 +101,8 @@ test('workflow setup uses a known Pantry CLI instead of resolving latest', () => | ||
| 101 | 101 | for (const [name, job] of Object.entries(workflow.jobs)) { |
| 102 | 102 | for (const step of job.steps ?? []) { |
| 103 | 103 | if (step.uses?.startsWith('pantry-pm/pantry/packages/action@')) { |
| 104 | expect(step.with?.version, `${file} ${name} must pin Pantry CLI`).toBe('0.11.93') | |
| 105 | expect(step.uses, `${file} ${name} must pin the matching action source`).toBe('pantry-pm/pantry/packages/action@e4b2d439c174c41d3e1c99b5743acb5789e92231') | |
| 104 | expect(step.with?.version, `${file} ${name} must pin Pantry CLI`).toBe('0.11.94') | |
| 105 | expect(step.uses, `${file} ${name} must pin the matching action source`).toBe('pantry-pm/pantry/packages/action@da17bb9317e8f8c247618fb176fe97269f15bfe4') | |
| 106 | 106 | } |
| 107 | 107 | } |
| 108 | 108 | } |
| @@ -250,7 +250,7 @@ test('a failed native leg cannot publish a partial GitHub, Zig, or npm release', | ||
| 250 | 250 | expect(release.jobs.npm.if).toContain("needs.prepare-npm.result == 'success'") |
| 251 | 251 | expect(release.jobs.npm.if).toContain("needs.publish-release.result == 'success'") |
| 252 | 252 | const zigPublish = registry.steps!.find(step => step.name === 'Publish Zig package') |
| 253 | expect(zigPublish?.with).toMatchObject({ version: '0.11.93', install: 'false', publish: 'zig', 'package-dir': 'packages/zig' }) | |
| 253 | expect(zigPublish?.with).toMatchObject({ version: '0.11.94', install: 'false', publish: 'zig', 'package-dir': 'packages/zig' }) | |
| 254 | 254 | expect(zigPublish?.with?.release).toBeUndefined() |
| 255 | 255 | expect(Object.entries(release.jobs).flatMap(([name, job]) => |
| 256 | 256 | (job.steps ?? []).filter(step => step.with?.publish === 'zig').map(() => name))).toEqual(['publish-pantry']) |