From 66e1b0187bfad424b4e12fe0f19f531bbd216e5b Mon Sep 17 00:00:00 2001 From: Aliaksandr Babrykovich Date: Sun, 25 Oct 2026 12:40:16 +0100 Subject: [PATCH] fix(e2e): give render enough time to mount the first WebView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Android e2e job fails `prewarm › accepts idle SDK readiness after the loading deadline` on master and on every PR run with `Render timeout: Element did not mount within 1000ms`. No `render` call in the prewarm, example or auth flow suites passed a timeout, so each used the harness default of 1s. The failing test is the first in its file: its render creates the process's first native WebView, which on the CI Android emulator takes longer than that. Later renders get a warm WebView and fit, which is why only that one test fails and iOS never does. Every render in those suites now passes a 10s mount timeout, matching the dark theme suite, so a change in test order cannot move the failure elsewhere. What the tests assert is unchanged. Co-Authored-By: Claude Opus 5.5 --- __e2e__/authFlow.harness.js | 12 ++++++++---- __e2e__/example.harness.js | 8 ++++++-- __e2e__/prewarm.harness.js | 23 +++++++++++++---------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/__e2e__/authFlow.harness.js b/__e2e__/authFlow.harness.js index 3f10762..3aae366 100644 --- a/__e2e__/authFlow.harness.js +++ b/__e2e__/authFlow.harness.js @@ -3,11 +3,15 @@ import { screen, userEvent } from '@react-native-harness/ui'; import React from 'react'; import PrewarmAuthApp, { authServer } from './PrewarmAuthApp'; +// `render` gives up after 1s by default, and the first render in a process creates the first +// native WebView, which on the CI Android emulator takes longer than that. +const MOUNT_OPTIONS = { timeout: 10000 }; + describe('live SDK multi-screen auth flow', () => { for (const passiveSiteKey of [false, true]) { test(`signs up and logs in after destroying the warmed WebView (passiveSiteKey=${passiveSiteKey})`, async () => { await fetch(`${authServer}/reset`, { method: 'POST' }); - await render(); + await render(, MOUNT_OPTIONS); await userEvent.type(await screen.findByTestId('email'), 'destroy@example.test'); for (const [attempt, mode] of ['signup', 'login'].entries()) { await userEvent.press(await screen.findByTestId(mode)); @@ -30,7 +34,7 @@ describe('live SDK multi-screen auth flow', () => { for (const passiveSiteKey of [false, true]) { test(`prewarms, signs up, and logs in without reloading (passiveSiteKey=${passiveSiteKey})`, async () => { await fetch(`${authServer}/reset`, { method: 'POST' }); - await render(); + await render(, MOUNT_OPTIONS); await waitFor( async () => { const state = await (await fetch(`${authServer}/state`)).json(); @@ -75,7 +79,7 @@ describe('live SDK multi-screen auth flow', () => { test('cancels queued verification and reuses the warmed SDK on retry', async () => { await fetch(`${authServer}/reset`, { method: 'POST' }); - await render(); + await render(, MOUNT_OPTIONS); await userEvent.type(await screen.findByTestId('email'), 'cancel@example.test'); await userEvent.press(await screen.findByTestId('signup')); await userEvent.press(await screen.findByTestId('cancel')); @@ -96,7 +100,7 @@ describe('live SDK multi-screen auth flow', () => { test('fetches a fresh token after the backend rejects credentials', async () => { await fetch(`${authServer}/reset`, { method: 'POST' }); const measurements = { attempts: [], errors: [], authErrors: [] }; - await render(); + await render(, MOUNT_OPTIONS); await userEvent.type(await screen.findByTestId('email'), 'retry@example.test'); for (const [mode, password, tokenCount] of [['signup', 'Password-123', 1], ['login', 'Password', 2]]) { await userEvent.press(await screen.findByTestId(mode)); diff --git a/__e2e__/example.harness.js b/__e2e__/example.harness.js index c6d48e2..4289f95 100644 --- a/__e2e__/example.harness.js +++ b/__e2e__/example.harness.js @@ -6,6 +6,10 @@ import App from '../App'; import AutopassExample from './ExampleAutopass'; import { authServer } from './PrewarmAuthApp'; +// `render` gives up after 1s by default, and the first render in a process creates the first +// native WebView, which on the CI Android emulator takes longer than that. +const MOUNT_OPTIONS = { timeout: 10000 }; + async function saveScreenshot(phase) { const screenshot = await screen.screenshot(); expect(screenshot).not.toBeNull(); @@ -35,7 +39,7 @@ describe('original repository example', () => { afterEach(() => log.mockRestore()); test('opens, dismisses, and reopens the live demo challenge', async () => { - await render(); + await render(, MOUNT_OPTIONS); for (const attempt of [1, 2]) { await launchExample(log); await waitFor(() => expect(log.mock.calls.filter(([message]) => message === 'Visual challenge opened')).toHaveLength(attempt), { timeout: 30000 }); @@ -56,7 +60,7 @@ describe('original repository example', () => { // Generated from Example.App.js; only the sitekey changes to the public auto-pass key. test('displays valid tokens and allows a second launch with the auto-pass key', async () => { await fetch(`${authServer}/reset`, { method: 'POST' }); - await render(); + await render(, MOUNT_OPTIONS); for (const attempt of [1, 2]) { await launchExample(log); await waitFor(() => expect(log.mock.calls.filter(([message]) => message === 'Verified code from hCaptcha')).toHaveLength(attempt), { timeout: 30000 }); diff --git a/__e2e__/prewarm.harness.js b/__e2e__/prewarm.harness.js index c45357a..acd12d0 100644 --- a/__e2e__/prewarm.harness.js +++ b/__e2e__/prewarm.harness.js @@ -39,6 +39,9 @@ const sdk = ` window.hCaptchaOnLoad(); `; const jsSrc = `data:application/javascript,${encodeURIComponent(sdk)}#`; +// `render` gives up after 1s by default, and the first render in a process creates the first +// native WebView, which on the CI Android emulator takes longer than that. +const MOUNT_OPTIONS = { timeout: 10000 }; // Native process startup is separate from the library's 15-second loading deadline. const sdkReadyOptions = { timeout: 30000 }; const waitForSdk = (testId) => waitFor(() => expect(screen.queryByTestId(testId)).not.toBeNull(), sdkReadyOptions); @@ -115,7 +118,7 @@ describe('native WebView prewarm', () => { const lateSdk = `setTimeout(function() { ${sdk} }, 16000);`; await render( events.push(event)} />); + onEvent={(event) => events.push(event)} />, MOUNT_OPTIONS); await waitForSdk('loads-1'); expect(events.map((event) => event.nativeEvent.data)).toEqual(['fixture-ready']); ref.current.execute(); @@ -130,7 +133,7 @@ describe('native WebView prewarm', () => { const events = []; await render( events.push(event)} - sdkSource={`data:application/javascript,${encodeURIComponent('window.fixtureRenderFailures = 2;\n' + sdk)}#`} />); + sdkSource={`data:application/javascript,${encodeURIComponent('window.fixtureRenderFailures = 2;\n' + sdk)}#`} />, MOUNT_OPTIONS); await waitFor(async () => { const state = await (await fetch(`${authServer}/state`)).json(); expect(state.sdkReady).toBe(1); @@ -151,7 +154,7 @@ describe('native WebView prewarm', () => { const key = `hidden-reset-${variant}`; const props = { captchaRef: ref, delayedTokens: true, inline: variant === 'inline', passiveSiteKey: variant === 'passive', onEvent: (event) => events.push(event) }; - const fixture = await render(); + const fixture = await render(, MOUNT_OPTIONS); await waitForSdk('loads-1'); ref.current.execute(); await waitFor(() => expect(events.filter((event) => event.markUsed)).toHaveLength(1), { timeout: 10000 }); @@ -180,7 +183,7 @@ describe('native WebView prewarm', () => { const props = { captchaRef: ref, inline: variant === 'inline', passiveSiteKey: variant === 'passive', onEvent: (event) => events.push(event) }; const fixture = await render(); + sdkSource={`${authServer}/slow/1/api.js`} />, MOUNT_OPTIONS); await waitFor(async () => { const state = await (await fetch(`${authServer}/state`)).json(); expect(state.sdkLoads).toBe(1); @@ -203,7 +206,7 @@ describe('native WebView prewarm', () => { const events = []; await render( events.push(event)} />); + delayedTokens={phase === 'pending'} hideAfterToken={false} onEvent={(event) => events.push(event)} />, MOUNT_OPTIONS); await waitForSdk('loads-1'); if (phase !== 'ready') { ref.current.execute(); @@ -231,7 +234,7 @@ describe('native WebView prewarm', () => { for (const passiveSiteKey of [false, true]) { test(`loads while hidden and reuses the widget (passiveSiteKey=${passiveSiteKey})`, async () => { - await render(); + await render(, MOUNT_OPTIONS); await waitForSdk('ready'); for (const attempt of [1, 2]) { await userEvent.press(await screen.findByTestId('execute')); @@ -241,7 +244,7 @@ describe('native WebView prewarm', () => { }); test(`rejects a cancelled outcome after re-execute (passiveSiteKey=${passiveSiteKey})`, async () => { - await render(); + await render(, MOUNT_OPTIONS); await waitForSdk('ready'); await userEvent.press(await screen.findByTestId('execute')); await new Promise((resolve) => setTimeout(resolve, 2500)); @@ -251,7 +254,7 @@ describe('native WebView prewarm', () => { }); test(`preserves a held token across presentation change (passiveSiteKey=${passiveSiteKey})`, async () => { - const fixture = await render(); + const fixture = await render(, MOUNT_OPTIONS); await waitForSdk('ready'); await userEvent.press(await screen.findByTestId('execute')); await waitFor(() => screen.findByTestId('fixture-token-widget-1-attempt-1-000000000000')); @@ -263,7 +266,7 @@ describe('native WebView prewarm', () => { } test('recreates the native document after a theme change', async () => { - const fixture = await render(); + const fixture = await render(, MOUNT_OPTIONS); await waitForSdk('loads-1'); await userEvent.press(await screen.findByTestId('execute')); await waitFor(() => screen.findByTestId('fixture-token-widget-1-attempt-1-000000000000')); @@ -278,7 +281,7 @@ describe('native WebView prewarm', () => { test(`updates verification data with the expected reload behavior (legacy=${legacy})`, async () => { const initial = legacy ? { rqdata: 'initial' } : { verifyParams: { rqdata: 'initial' } }; const updated = legacy ? { rqdata: 'current-000000000000' } : { verifyParams: { rqdata: 'current-000000000000' } }; - const fixture = await render(); + const fixture = await render(, MOUNT_OPTIONS); await waitForSdk('loads-1'); await fixture.rerender(); const loads = legacy ? 2 : 1;