From 9e2102df6415b2bd05800ce39e59d89cb5426371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Mon, 29 Jun 2026 12:13:27 +0200 Subject: [PATCH 1/2] test(example): de-flake use-rive-trigger #230 (deterministic re-renders) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #230 test drove re-renders with setInterval(50ms)/300ms and asserted renderCount >= 3 within a 2000ms waitFor. waitFor resolves the instant renderCount hits 3, so it needs exactly 2 interval ticks — zero margin. When the JS thread is busy at mount (RiveView + dataBind init) the ticks are starved and only 1 lands, so renderCount stays at 2 and the test fails (the intermittent 'expected 2 to be greater than or equal to 3'). Drive the re-renders deterministically via an exposed context.rerender() instead of racing a wall-clock setInterval, removing the timing dependence. Keep the failure condition as a permanent regression guard: a 500ms JS-thread block at mount that starves any re-render driver. The old setInterval version fails 8/8 under it; the deterministic version passes 8/8 — so every run now verifies the fix against the reproduction. --- .../__tests__/use-rive-trigger.harness.tsx | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/example/__tests__/use-rive-trigger.harness.tsx b/example/__tests__/use-rive-trigger.harness.tsx index 88659383..f4a51ab9 100644 --- a/example/__tests__/use-rive-trigger.harness.tsx +++ b/example/__tests__/use-rive-trigger.harness.tsx @@ -39,10 +39,17 @@ type TriggerContext = { triggerFn: (() => void) | null; error: Error | null; renderCount: number; + rerender: (() => void) | null; }; function createTriggerContext(): TriggerContext { - return { triggerCount: 0, triggerFn: null, error: null, renderCount: 0 }; + return { + triggerCount: 0, + triggerFn: null, + error: null, + renderCount: 0, + rerender: null, + }; } // ─── Test component: stable callback ─────────────────────────────── @@ -110,14 +117,19 @@ function UnstableTriggerComponent({ context.error = error; }, [context, trigger, error]); - // Force re-renders to change callback identity + // Re-render on demand so the test can change the callback identity deterministically. + useEffect(() => { + context.rerender = () => setTick((t) => t + 1); + }, [context]); + + // Regression guard: block the JS thread at mount (as RiveView init does) to starve any + // re-render driver. The old setInterval-based version flaked here (renderCount stuck at 2); + // the deterministic re-renders above survive it. useEffect(() => { - const interval = setInterval(() => setTick((t) => t + 1), 50); - const timeout = setTimeout(() => clearInterval(interval), 300); - return () => { - clearInterval(interval); - clearTimeout(timeout); - }; + const end = Date.now() + 500; + while (Date.now() < end) { + /* simulate heavy mount-time work */ + } }, []); return ( @@ -182,14 +194,20 @@ describe('useRiveTrigger hook', () => { /> ); - // Wait for the re-render burst to complete (300ms of re-renders every 50ms) await waitFor( () => { - expect(context.renderCount).toBeGreaterThanOrEqual(3); + expect(context.rerender).not.toBeNull(); }, - { timeout: 2000 } + { timeout: 3000 } ); + // Re-render several times to churn the callback identity. + for (let i = 0; i < 3; i++) { + context.rerender!(); + await new Promise((r) => setTimeout(r, 0)); + } + expect(context.renderCount).toBeGreaterThanOrEqual(3); + await waitFor( () => { expect(context.triggerFn).not.toBeNull(); From 2e2b9752516cca6012f83b68cf90216adb36052d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Fri, 26 Jun 2026 14:41:49 +0200 Subject: [PATCH 2/2] ci(ios): pin iOS jobs to macos-15 to stabilize simulator boot macos-latest is mid-migration to macOS 26. Jobs randomly land on macos-15-arm64 or macos-26-arm64, and only the macOS 15 image has a pre-created iPhone 16 Pro on an iOS >=26 runtime. When a job lands on macos-26 the simulator-action boot step fails with 'No devices found', which is why the same commit passes on one run and fails on another. Pin build-ios and test-harness-ios to macos-15 for deterministic runs. --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75c2faa7..1000828f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,7 +140,9 @@ jobs: yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" build-ios: - runs-on: macos-latest + # Pinned: macos-latest is mid-migration to macOS 26, whose simulator set + # has no iPhone 16 Pro on an iOS >=26 runtime, breaking the harness boot. + runs-on: macos-15 env: XCODE_VERSION: 26.3 TURBO_CACHE_DIR: .turbo/ios @@ -208,7 +210,9 @@ jobs: yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" test-harness-ios: - runs-on: macos-latest + # Pinned to macos-15 — see build-ios. macos-26 runners lack a matching + # iPhone 16 Pro / iOS >=26 simulator, so the boot step fails intermittently. + runs-on: macos-15 timeout-minutes: 60 env: XCODE_VERSION: 26.3