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 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();