diff --git a/.github/workflows/scripts/boot-simulator.sh b/.github/workflows/scripts/boot-simulator.sh index 739956df07..fceb97dd48 100755 --- a/.github/workflows/scripts/boot-simulator.sh +++ b/.github/workflows/scripts/boot-simulator.sh @@ -42,6 +42,51 @@ describe_booted_device() { || true } +resolve_device_udid() { + local device="$1" + local udid + + udid="$( + xcrun simctl list devices booted 2>/dev/null \ + | grep -F "${device} (" \ + | grep -v 'unavailable' \ + | head -1 \ + | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/' \ + || true + )" + if [[ -n "$udid" ]]; then + echo "$udid" + return 0 + fi + + udid="$( + xcrun simctl list devices available 2>/dev/null \ + | grep -F "${device} (" \ + | grep -v 'unavailable' \ + | head -1 \ + | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/' \ + || true + )" + echo "$udid" +} + +kill_resolved_simulator() { + local device="$1" + local udid + + udid="$(resolve_device_udid "$device")" + if [[ -z "$udid" ]]; then + log_boot_status "phase=kill_resolved device=\"${device}\" not found, skipping" + return 0 + fi + + log_boot_status "phase=kill_resolved udid=${udid} device=\"${device}\"" + killall Simulator 2>/dev/null || true + xcrun simctl terminate "$udid" com.invertase.testing 2>/dev/null || true + xcrun simctl shutdown "$udid" 2>/dev/null || true + xcrun simctl shutdown "$device" 2>/dev/null || true +} + log_migration_status() { local device="$1" local migration_output probe_rc @@ -117,10 +162,8 @@ popd >/dev/null || exit 1 log_boot_status "phase=resolve_device name=\"${SIM}\" (from tests/.detoxrc.js)" -# Clear up any existing attempts in case we are re-trying -log_boot_status "phase=shutdown_existing killing Simulator.app if running..." -killall Simulator 2>/dev/null || true -xcrun simctl shutdown "$SIM" 2>/dev/null || true +# Kill the resolved simulator first when present (CI pre-boot and e2e Jet retries). +kill_resolved_simulator "$SIM" log_boot_status "phase=boot_command starting simctl boot..." set +e diff --git a/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch b/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch index 4bccb154b5..5763cd934a 100644 --- a/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch +++ b/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch @@ -1,8 +1,54 @@ diff --git a/dist/Server.js b/dist/Server.js -index ad9debe2086ab9b96e97a69aec966da8114ad102..e3c1c964956023df876dfcac3f73000b4eaf1bba 100644 +index ad9debe2086ab9b96e97a69aec966da8114ad102..c8f4e2a1b9d3f6e8a7c5d4b2e1f0a9c8b7d6e5f4 100644 --- a/dist/Server.js +++ b/dist/Server.js -@@ -130,6 +130,17 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter { +@@ -73,6 +73,8 @@ + /** The options to send to the next connecting running client */ + this.clientOptions = {}; + this._listening = false; ++ this._clientResetTimer = null; ++ this._awaitingInitialClientRun = false; + this.handleConnection = (ws, req) => { + this.debug("Client connected"); + // Check that the protocol matches +@@ -88,6 +90,11 @@ + ws.close(1002, `Expected "${expectedProtocol}" protocol got "${ws.protocol}"`); + return; + } ++ if (this._clientResetTimer) { ++ clearTimeout(this._clientResetTimer); ++ this._clientResetTimer = null; ++ console.warn(`[mocha-remote-ws] reconnect_recovered preserving_runner`); ++ } + if (this.client) { + this.debug("A client was already connected"); + this.client.close(1013 /* try again later */, "Got a connection from another client"); +@@ -97,14 +104,20 @@ + // Hang onto the client + this.client = ws; + this.client.on("message", this.handleMessage.bind(this, this.client)); +- this.client.once("close", this.handleReset); ++ this.client.once("close", (code) => this.handleClientDisconnect(code)); + // If we already have a runner, it can run now that we have a client + if (this.runner) { +- if (this.clientOptions) { +- this.send({ action: "run", options: this.clientOptions }); ++ if (this._awaitingInitialClientRun) { ++ if (this.clientOptions) { ++ this.send({ action: "run", options: this.clientOptions }); ++ this._awaitingInitialClientRun = false; ++ } ++ else { ++ throw new Error("Internal error: Expected a clientOptions"); ++ } + } + else { +- throw new Error("Internal error: Expected a clientOptions"); ++ this.debug("Client reconnected while runner active; resuming without re-run"); + } + } + else if (this.config.autoRun) { +@@ -130,6 +143,17 @@ throw new Error("Received a message from the client, but server wasn't running"); } } @@ -20,3 +66,58 @@ index ad9debe2086ab9b96e97a69aec966da8114ad102..e3c1c964956023df876dfcac3f73000b else if (msg.action === "error") { if (typeof msg.message !== "string") { throw new Error("Expected 'error' action to have an error argument with a message"); +@@ -149,13 +173,46 @@ + } + else { + throw err; ++ } ++ } ++ }; ++ this.handleClientDisconnect = (code) => { ++ const transientCodes = [1006, 1001]; ++ const graceMs = this.config.reconnectGraceMs ?? 15000; ++ if (this.runner && transientCodes.includes(code)) { ++ if (this._clientResetTimer) { ++ clearTimeout(this._clientResetTimer); + } ++ console.warn(`[mocha-remote-ws] transient_disconnect code=${code} grace_ms=${graceMs} preserving_runner`); ++ const client = this.client; ++ delete this.client; ++ if (client) { ++ client.removeAllListeners(); ++ } ++ this._clientResetTimer = setTimeout(() => { ++ this._clientResetTimer = null; ++ if (!this.client) { ++ console.error(`[mocha-remote-ws] fatal_disconnect code=${code} grace_expired_ms=${graceMs}`); ++ this.handleReset(); ++ } ++ }, graceMs); ++ return; + } ++ if (this._clientResetTimer) { ++ clearTimeout(this._clientResetTimer); ++ this._clientResetTimer = null; ++ } ++ this.handleReset(); + }; + /** + * Resets the server for another test run. + */ + this.handleReset = () => { ++ if (this._clientResetTimer) { ++ clearTimeout(this._clientResetTimer); ++ this._clientResetTimer = null; ++ } ++ this._awaitingInitialClientRun = false; + // Forget everything about the runner and the client + const { runner, client } = this; + delete this.runner; +@@ -263,6 +320,7 @@ + // this.runner = new Mocha.Runner(this.suite, this.options.delay || false); + // TODO: Stub this to match the Runner's interface even better + this.runner = new FakeRunner_1.FakeRunner(); ++ this._awaitingInitialClientRun = true; + // Attach event listeners to update stats + (0, stats_collector_1.createStatsCollector)(this.runner); + // Set the client options, to be passed to the next running client diff --git a/eslint.config.mjs b/eslint.config.mjs index aeeef28e43..8be8c6aeaa 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -62,7 +62,7 @@ export default defineConfig([ }, { name: 'Prettier', - ...eslintPluginPrettierRecommended.recommended, + ...eslintPluginPrettierRecommended, }, { name: 'React', diff --git a/packages/ai/lib/requests/request.ts b/packages/ai/lib/requests/request.ts index 84caf395c4..0d9d9bb596 100644 --- a/packages/ai/lib/requests/request.ts +++ b/packages/ai/lib/requests/request.ts @@ -125,7 +125,7 @@ function createAbortError(reason?: unknown): Error { } function getAbortSignalReason(signal?: AbortSignal): unknown { - return (signal as AbortSignal & { reason?: unknown } | undefined)?.reason; + return (signal as (AbortSignal & { reason?: unknown }) | undefined)?.reason; } export class TemplateRequestUrl { diff --git a/packages/analytics/e2e/analytics.e2e.js b/packages/analytics/e2e/analytics.e2e.js index 13eda3d913..1886aa9ba6 100644 --- a/packages/analytics/e2e/analytics.e2e.js +++ b/packages/analytics/e2e/analytics.e2e.js @@ -15,12 +15,67 @@ * */ describe('analytics()', function () { - beforeEach(async function () { - const { getAnalytics, logEvent } = analyticsModular; - await logEvent(getAnalytics(), 'screen_view'); + // Probe session ID before any other analytics API usage (including suite beforeEach). + // Namespace APIs not tested here as only one set can run reliably, and modular-only is the future state. + describe('getSessionId() [modular — runs before all other analytics tests]', function () { + it('calls native fn without error', async function () { + const { getAnalytics, getSessionId } = analyticsModular; + await getSessionId(getAnalytics()); + }); + + it('returns a non empty session ID', async function () { + if (Platform.other) { + this.skip(); + } + const { getAnalytics, getSessionId } = analyticsModular; + let sessionId = await getSessionId(getAnalytics()); + // On iOS it can take ~ 3 minutes for the session ID to be generated + // Otherwise, `Analytics uninitialized` error will be thrown + // On CI we're only going to give it a few tries + const retries = global.isCI ? 3 : 240; + let attemptsLeft = retries; + while (!sessionId && attemptsLeft > 0) { + await Utils.sleep(1000); + sessionId = await getSessionId(getAnalytics()); + attemptsLeft -= 1; + } + + // on CI this will be ignored so it doesn't flake + if (!sessionId && global.isCI) { + this.skip(); + return; + } + if (!sessionId) { + return Promise.reject( + new Error('Firebase SDK did not return a session ID after 4 minutes'), + ); + } + + sessionId.should.not.equal(0); + }); + + it('returns a null value if session expires', async function () { + if (Platform.ios) { + // TODO - 20251030 iOS no longer correctly expires sessions + this.skip(); + } + const { getAnalytics, getSessionId, setSessionTimeoutDuration } = analyticsModular; + + // Set session duration to 1 millisecond + setSessionTimeoutDuration(getAnalytics(), 1); + // Wait 100 millisecond to ensure session expires + await Utils.sleep(100); + const sessionId = await getSessionId(getAnalytics()); + should.equal(sessionId, null); + }); }); describe('firebase v8 compatibility', function () { + beforeEach(async function () { + const { getAnalytics, logEvent } = analyticsModular; + await logEvent(getAnalytics(), 'screen_view'); + }); + beforeEach(async function beforeEachTest() { // @ts-ignore globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; @@ -62,55 +117,6 @@ describe('analytics()', function () { }); }); - describe('getSessionId()', function () { - it('calls native fn without error', async function () { - await firebase.analytics().getSessionId(); - }); - - it('returns a non empty session ID', async function () { - if (Platform.other) { - this.skip(); - } - let sessionId = await firebase.analytics().getSessionId(); - // On iOS it can take ~ 3 minutes for the session ID to be generated - // Otherwise, `Analytics uninitialized` error will be thrown - // On CI we're only going to give it a few tries - const retries = global.isCI ? 3 : 240; - let attemptsLeft = retries; - while (!sessionId && attemptsLeft > 0) { - await Utils.sleep(1000); - sessionId = await firebase.analytics().getSessionId(); - attemptsLeft -= 1; - } - - // on CI this will be ignored so it doesn't flake - if (!sessionId && global.isCI) { - this.skip(); - return; - } - if (!sessionId) { - return Promise.reject( - new Error('Firebase SDK did not return a session ID after 4 minutes'), - ); - } - - sessionId.should.not.equal(0); - }); - - it('returns a null value if session expires', async function () { - if (Platform.ios) { - // TODO - 20251030 iOS no longer correctly expires sessions - this.skip(); - } - // Set session duration to 1 millisecond - firebase.analytics().setSessionTimeoutDuration(1); - // Wait 100 millisecond to ensure session expires - await Utils.sleep(100); - const sessionId = await firebase.analytics().getSessionId(); - should.equal(sessionId, null); - }); - }); - describe('setUserId()', function () { it('allows a null values to be set', async function () { await firebase.analytics().setUserId(null); @@ -569,6 +575,11 @@ describe('analytics()', function () { }); describe('modular', function () { + beforeEach(async function () { + const { getAnalytics, logEvent } = analyticsModular; + await logEvent(getAnalytics(), 'screen_view'); + }); + describe('getAnalytics', function () { it('pass app as argument', function () { const { getApp } = modular; @@ -997,59 +1008,6 @@ describe('analytics()', function () { }); }); - describe('getSessionId()', function () { - it('calls native fn without error', async function () { - const { getAnalytics, getSessionId } = analyticsModular; - await getSessionId(getAnalytics()); - }); - - it('returns a non empty session ID', async function () { - if (Platform.other) { - this.skip(); - } - const { getAnalytics, getSessionId } = analyticsModular; - let sessionId = await getSessionId(getAnalytics()); - // On iOS it can take ~ 3 minutes for the session ID to be generated - // Otherwise, `Analytics uninitialized` error will be thrown - // On CI we're only going to give it a few tries - const retries = global.isCI ? 3 : 240; - let attemptsLeft = retries; - while (!sessionId && attemptsLeft > 0) { - await Utils.sleep(1000); - sessionId = await getSessionId(getAnalytics()); - attemptsLeft -= 1; - } - - // on CI this will be ignored so it doesn't flake - if (!sessionId && global.isCI) { - this.skip(); - return; - } - if (!sessionId) { - return Promise.reject( - new Error('Firebase SDK did not return a session ID after 4 minutes'), - ); - } - - sessionId.should.not.equal(0); - }); - - it('returns a null value if session expires', async function () { - if (Platform.ios) { - // TODO - 20251030 iOS no longer correctly expires sessions - this.skip(); - } - const { getAnalytics, getSessionId, setSessionTimeoutDuration } = analyticsModular; - - // Set session duration to 1 millisecond - setSessionTimeoutDuration(getAnalytics(), 1); - // Wait 100 millisecond to ensure session expires - await Utils.sleep(100); - const sessionId = await getSessionId(getAnalytics()); - should.equal(sessionId, null); - }); - }); - describe('setUserId()', function () { it('allows a null values to be set', async function () { const { getAnalytics, setUserId } = analyticsModular; diff --git a/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m b/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m index 2d20d58433..7ecfb5bbb6 100644 --- a/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m +++ b/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m @@ -165,7 +165,7 @@ - (dispatch_queue_t)methodQueue { return; } completed = YES; - DLog(@"Error getting session ID: timed out after 60 seconds"); + DLog(@"getSessionId timed_out: no SDK callback within 60 seconds"); resolve([NSNull null]); }); @@ -178,16 +178,18 @@ - (dispatch_queue_t)methodQueue { // Occasionally sessionID is 0 despite nil error, reject as if it were an error // https://github.com/firebase/firebase-ios-sdk/issues/15258 if (!error && [NSNumber numberWithLongLong:sessionID] == 0) { - DLog(@"Error getting session ID: sessionID is zero despite nil error"); + DLog(@"getSessionId zero_without_error: sessionID=0 (firebase-ios-sdk#15258)"); return resolve([NSNull null]); } if (error) { - DLog(@"Error getting session ID: %@", error); + DLog(@"getSessionId sdk_error: domain=%@ code=%ld description=%@", error.domain, + (long)error.code, error.localizedDescription ?: @"(none)"); return resolve([NSNull null]); - } else { - return resolve([NSNumber numberWithLongLong:sessionID]); } + + DLog(@"getSessionId success: sessionID=%lld", sessionID); + return resolve([NSNumber numberWithLongLong:sessionID]); }]; } diff --git a/packages/analytics/plugin/src/ios/index.ts b/packages/analytics/plugin/src/ios/index.ts index e642d277ac..cdf924da04 100644 --- a/packages/analytics/plugin/src/ios/index.ts +++ b/packages/analytics/plugin/src/ios/index.ts @@ -1,3 +1,6 @@ -import { withIosWithoutAdIdSupport, withIosGoogleAppMeasurementOnDeviceConversion } from './podfile'; +import { + withIosWithoutAdIdSupport, + withIosGoogleAppMeasurementOnDeviceConversion, +} from './podfile'; export { withIosWithoutAdIdSupport, withIosGoogleAppMeasurementOnDeviceConversion }; diff --git a/packages/auth/__tests__/auth.test.ts b/packages/auth/__tests__/auth.test.ts index f6672c1204..14a3c067e7 100644 --- a/packages/auth/__tests__/auth.test.ts +++ b/packages/auth/__tests__/auth.test.ts @@ -1340,7 +1340,9 @@ describe('Auth', function () { _setUserCredential: setUserCredential, }; const user = new User(authInternal as any, { uid: 'test-uid' } as any); - await user.reauthenticateWithRedirect({ toObject: () => ({ providerId: 'google.com' }) } as any); + await user.reauthenticateWithRedirect({ + toObject: () => ({ providerId: 'google.com' }), + } as any); expect(setUserCredential).toHaveBeenCalled(); }); }); diff --git a/packages/auth/lib/ActionCodeURL.ts b/packages/auth/lib/ActionCodeURL.ts index 7416b16b92..639333377b 100644 --- a/packages/auth/lib/ActionCodeURL.ts +++ b/packages/auth/lib/ActionCodeURL.ts @@ -42,12 +42,8 @@ function querystringDecode(query: string): Record { } const separatorIndex = part.indexOf('='); - const key = decodeURIComponent( - separatorIndex >= 0 ? part.slice(0, separatorIndex) : part, - ); - const value = decodeURIComponent( - separatorIndex >= 0 ? part.slice(separatorIndex + 1) : '', - ); + const key = decodeURIComponent(separatorIndex >= 0 ? part.slice(0, separatorIndex) : part); + const value = decodeURIComponent(separatorIndex >= 0 ? part.slice(separatorIndex + 1) : ''); decoded[key] = value; } @@ -81,9 +77,7 @@ function parseMode(mode: string | null): string | null { */ function parseDeepLink(url: string): string { const link = querystringDecode(extractQuerystring(url)).link; - const doubleDeepLink = link - ? querystringDecode(extractQuerystring(link)).deep_link_id - : null; + const doubleDeepLink = link ? querystringDecode(extractQuerystring(link)).deep_link_id : null; const iOSDeepLink = querystringDecode(extractQuerystring(url)).deep_link_id; const iOSDoubleDeepLink = iOSDeepLink ? querystringDecode(extractQuerystring(iOSDeepLink)).link diff --git a/packages/auth/lib/User.ts b/packages/auth/lib/User.ts index 14b2e1bd91..735a935f7c 100644 --- a/packages/auth/lib/User.ts +++ b/packages/auth/lib/User.ts @@ -241,8 +241,8 @@ export default class User { } return this._auth.native.sendEmailVerification(actionCodeSettings).then(user => { - this._auth._setUser(user); - }); + this._auth._setUser(user); + }); } toJSON(): object { @@ -250,15 +250,13 @@ export default class User { } unlink(providerId: string): Promise { - return this._auth.native - .unlink(providerId) - .then(user => { - const updatedUser = this._auth._setUser(user); - if (!updatedUser) { - throw new Error('firebase.auth.User.unlink() returned no user after unlinking provider.'); - } - return updatedUser; - }); + return this._auth.native.unlink(providerId).then(user => { + const updatedUser = this._auth._setUser(user); + if (!updatedUser) { + throw new Error('firebase.auth.User.unlink() returned no user after unlinking provider.'); + } + return updatedUser; + }); } updateEmail(email: string): Promise { @@ -361,8 +359,8 @@ export default class User { } return this._auth.native.verifyBeforeUpdateEmail(newEmail, actionCodeSettings).then(user => { - this._auth._setUser(user); - }); + this._auth._setUser(user); + }); } /** diff --git a/packages/auth/lib/credentials/EmailAuthCredential.ts b/packages/auth/lib/credentials/EmailAuthCredential.ts index deed376f5d..94c3f8366c 100644 --- a/packages/auth/lib/credentials/EmailAuthCredential.ts +++ b/packages/auth/lib/credentials/EmailAuthCredential.ts @@ -25,11 +25,7 @@ type EmailCredentialJSON = { }; export class EmailAuthCredential extends AuthCredential { - constructor( - signInMethod: 'password' | 'emailLink', - email: string, - password: string, - ) { + constructor(signInMethod: 'password' | 'emailLink', email: string, password: string) { super(signInMethod, signInMethod, email, password); } diff --git a/packages/auth/lib/credentials/OAuthCredential.ts b/packages/auth/lib/credentials/OAuthCredential.ts index d346f2d62a..856a008b96 100644 --- a/packages/auth/lib/credentials/OAuthCredential.ts +++ b/packages/auth/lib/credentials/OAuthCredential.ts @@ -38,7 +38,10 @@ type OAuthCredentialParams = { bridgeSecret?: string; }; -function resolveOAuthBridgeFields(params: OAuthCredentialParams): { token: string; secret: string } { +function resolveOAuthBridgeFields(params: OAuthCredentialParams): { + token: string; + secret: string; +} { if (params.bridgeToken !== undefined || params.bridgeSecret !== undefined) { return { token: params.bridgeToken ?? '', diff --git a/packages/auth/lib/getMultiFactorResolver.ts b/packages/auth/lib/getMultiFactorResolver.ts index c5b4205264..eecf39e183 100644 --- a/packages/auth/lib/getMultiFactorResolver.ts +++ b/packages/auth/lib/getMultiFactorResolver.ts @@ -23,7 +23,9 @@ export function getMultiFactorResolver( error: ErrorWithResolver, ): FirebaseAuthTypes.MultiFactorResolver | null { if (isOther) { - return auth.native.getMultiFactorResolver(error) as FirebaseAuthTypes.MultiFactorResolver | null; + return auth.native.getMultiFactorResolver( + error, + ) as FirebaseAuthTypes.MultiFactorResolver | null; } if ( error.hasOwnProperty('userInfo') && diff --git a/packages/auth/lib/modular.ts b/packages/auth/lib/modular.ts index 9a4ed40a9b..8ec3628184 100644 --- a/packages/auth/lib/modular.ts +++ b/packages/auth/lib/modular.ts @@ -97,17 +97,9 @@ import type { type AnyFn = (...args: any[]) => any; type UserModuleInternal = UserInternal; -type MultiFactorInfoInternal = - | MultiFactorInfo - | MultiFactorResolverResultInternal['hints'][number]; +type MultiFactorInfoInternal = MultiFactorInfo | MultiFactorResolverResultInternal['hints'][number]; -export { - ActionCodeOperation, - FactorId, - OperationType, - ProviderId, - SignInMethod, -}; +export { ActionCodeOperation, FactorId, OperationType, ProviderId, SignInMethod }; function appWithAuth(app?: FirebaseApp): AppWithAuthInternal { return (app ? getApp(app.name) : getApp()) as unknown as AppWithAuthInternal; @@ -128,9 +120,7 @@ type AdditionalUserInfoSource = { username?: string | null; } & Record; -function normalizeAdditionalUserInfo( - info: AdditionalUserInfoSource, -): AdditionalUserInfoNative { +function normalizeAdditionalUserInfo(info: AdditionalUserInfoSource): AdditionalUserInfoNative { return { ...info, isNewUser: Boolean(info.isNewUser), @@ -245,9 +235,7 @@ function normalizeMultiFactorUser(multiFactorUser: MultiFactorUserResultInternal getSession: () => multiFactorUser.getSession(), enroll: (assertion, displayName) => multiFactorUser.enroll(assertion, displayName), unenroll: option => - multiFactorUser.unenroll( - option as Parameters[0], - ), + multiFactorUser.unenroll(option as Parameters[0]), }; } @@ -417,11 +405,15 @@ export function createUserWithEmailAndPassword( password: string, ): Promise { const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.createUserWithEmailAndPassword, email, password).then( - userCredential => - normalizeUserCredential(userCredential, { - operationType: OperationType.SIGN_IN, - }), + return callAuthMethod( + authInternal, + authInternal.createUserWithEmailAndPassword, + email, + password, + ).then(userCredential => + normalizeUserCredential(userCredential, { + operationType: OperationType.SIGN_IN, + }), ); } @@ -636,11 +628,15 @@ export function signInWithEmailAndPassword( password: string, ): Promise { const authInternal = getAuthInternal(auth); - return callAuthMethod(authInternal, authInternal.signInWithEmailAndPassword, email, password).then( - userCredential => - normalizeUserCredential(userCredential, { - operationType: OperationType.SIGN_IN, - }), + return callAuthMethod( + authInternal, + authInternal.signInWithEmailAndPassword, + email, + password, + ).then(userCredential => + normalizeUserCredential(userCredential, { + operationType: OperationType.SIGN_IN, + }), ); } @@ -1108,9 +1104,7 @@ export function verifyBeforeUpdateEmail( * * @remarks Returns firebase-js-sdk core fields plus any extra native keys copied from the bridge. */ -export function getAdditionalUserInfo( - userCredential: UserCredential, -): AdditionalUserInfo | null { +export function getAdditionalUserInfo(userCredential: UserCredential): AdditionalUserInfo | null { if (userCredential.additionalUserInfo) { return userCredential.additionalUserInfo; } diff --git a/packages/auth/lib/multiFactor.ts b/packages/auth/lib/multiFactor.ts index d95ffc7536..846ba65ded 100644 --- a/packages/auth/lib/multiFactor.ts +++ b/packages/auth/lib/multiFactor.ts @@ -1,8 +1,5 @@ import { reload } from './modular'; -import type { - MultiFactorAssertion as ModularMultiFactorAssertion, - User, -} from './types/auth'; +import type { MultiFactorAssertion as ModularMultiFactorAssertion, User } from './types/auth'; import type { FirebaseAuthTypes } from './types/namespaced'; import type { AuthInternal, MultiFactorEnrollmentAssertionInternal } from './types/internal'; diff --git a/packages/auth/lib/namespaced.ts b/packages/auth/lib/namespaced.ts index 6d11c08929..72cfb78195 100644 --- a/packages/auth/lib/namespaced.ts +++ b/packages/auth/lib/namespaced.ts @@ -623,7 +623,10 @@ class FirebaseAuthModule extends FirebaseModule { email: string, actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings, ): Promise { - return this.native.sendSignInLinkToEmail(email, this._resolveActionCodeSettings(actionCodeSettings)); + return this.native.sendSignInLinkToEmail( + email, + this._resolveActionCodeSettings(actionCodeSettings), + ); } isSignInWithEmailLink(emailLink: string): Promise { diff --git a/packages/auth/lib/providers/OIDCAuthProvider.ts b/packages/auth/lib/providers/OIDCAuthProvider.ts index 51cdd13ce0..5fd3be3f35 100644 --- a/packages/auth/lib/providers/OIDCAuthProvider.ts +++ b/packages/auth/lib/providers/OIDCAuthProvider.ts @@ -38,11 +38,7 @@ export default class OIDCAuthProvider { return providerId; } - static credential( - oidcSuffix: string, - idToken: string, - accessToken?: string, - ): AuthCredentialType { + static credential(oidcSuffix: string, idToken: string, accessToken?: string): AuthCredentialType { const resolvedProviderId = providerId + oidcSuffix; return new AuthCredential(resolvedProviderId, resolvedProviderId, idToken, accessToken ?? ''); } diff --git a/packages/auth/lib/types/internal.ts b/packages/auth/lib/types/internal.ts index 7f3231b237..ee2c2fdffb 100644 --- a/packages/auth/lib/types/internal.ts +++ b/packages/auth/lib/types/internal.ts @@ -329,9 +329,10 @@ export interface RNFBAuthModule { token: string, secret?: string | null, ): Promise; - updateProfile( - updates: { displayName?: string | null; photoURL?: string | null }, - ): Promise; + updateProfile(updates: { + displayName?: string | null; + photoURL?: string | null; + }): Promise; verifyBeforeUpdateEmail( newEmail: string, actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings, @@ -381,13 +382,12 @@ export type AuthInternal = Auth & { email: string, actionCodeSettings?: ActionCodeSettings | null, ): Promise; - sendSignInLinkToEmail( - email: string, - actionCodeSettings?: ActionCodeSettings, - ): Promise; + sendSignInLinkToEmail(email: string, actionCodeSettings?: ActionCodeSettings): Promise; setLanguageCode(code: string | null): Promise; signInAnonymously(): Promise; - signInWithCredential(credential: AuthCredential): Promise; + signInWithCredential( + credential: AuthCredential, + ): Promise; signInWithCustomToken(customToken: string): Promise; signInWithEmailAndPassword( email: string, @@ -443,8 +443,12 @@ export type UserInternal = FirebaseAuthTypes.User & { _auth?: AuthInternal; _user?: NativeUserInternal; getIdTokenResult(forceRefresh?: boolean): Promise; - linkWithCredential(credential: AuthCredential): Promise; - linkWithPopup(provider: AuthProviderWithObjectInternal): Promise; + linkWithCredential( + credential: AuthCredential, + ): Promise; + linkWithPopup( + provider: AuthProviderWithObjectInternal, + ): Promise; linkWithRedirect( provider: AuthProviderWithObjectInternal, ): Promise; diff --git a/packages/database/e2e/query/onChildAdded.e2e.js b/packages/database/e2e/query/onChildAdded.e2e.js index c70201ae00..b7a5620d21 100644 --- a/packages/database/e2e/query/onChildAdded.e2e.js +++ b/packages/database/e2e/query/onChildAdded.e2e.js @@ -15,12 +15,7 @@ * */ -const { - PATH, - seed, - wipe, - waitForNativeDbListenerRegistration, -} = require('../helpers'); +const { PATH, seed, wipe, waitForNativeDbListenerRegistration } = require('../helpers'); const TEST_PATH = `${PATH}/on`; diff --git a/packages/database/e2e/query/onChildChanged.e2e.js b/packages/database/e2e/query/onChildChanged.e2e.js index 2f10880455..a89ea29e1c 100644 --- a/packages/database/e2e/query/onChildChanged.e2e.js +++ b/packages/database/e2e/query/onChildChanged.e2e.js @@ -15,12 +15,7 @@ * */ -const { - PATH, - seed, - wipe, - waitForNativeDbListenerReady, -} = require('../helpers'); +const { PATH, seed, wipe, waitForNativeDbListenerReady } = require('../helpers'); const TEST_PATH = `${PATH}/on`; diff --git a/packages/database/e2e/query/onChildMoved.e2e.js b/packages/database/e2e/query/onChildMoved.e2e.js index 5247dcacfb..0de96c552f 100644 --- a/packages/database/e2e/query/onChildMoved.e2e.js +++ b/packages/database/e2e/query/onChildMoved.e2e.js @@ -15,12 +15,7 @@ * */ -const { - PATH, - seed, - wipe, - waitForNativeDbListenerRegistration, -} = require('../helpers'); +const { PATH, seed, wipe, waitForNativeDbListenerRegistration } = require('../helpers'); const TEST_PATH = `${PATH}/on`; diff --git a/packages/database/e2e/query/onChildRemoved.e2e.js b/packages/database/e2e/query/onChildRemoved.e2e.js index 709487dc42..fa29f2838f 100644 --- a/packages/database/e2e/query/onChildRemoved.e2e.js +++ b/packages/database/e2e/query/onChildRemoved.e2e.js @@ -15,12 +15,7 @@ * */ -const { - PATH, - seed, - wipe, - waitForNativeDbListenerReady, -} = require('../helpers'); +const { PATH, seed, wipe, waitForNativeDbListenerReady } = require('../helpers'); const TEST_PATH = `${PATH}/on`; diff --git a/packages/firestore/consumer-type-test.ts b/packages/firestore/consumer-type-test.ts index f8bbcbb209..0155d604fb 100644 --- a/packages/firestore/consumer-type-test.ts +++ b/packages/firestore/consumer-type-test.ts @@ -314,9 +314,7 @@ const nsDocRef = nsColl.doc('alice'); const nsQuery = nsColl.where('name', '==', 'test'); nsDocRef.set({ name: 'Alice', count: 1 }).then(() => {}); -nsDocRef - .set({ name: 'Alice' }, { merge: true }) - .then(() => {}); +nsDocRef.set({ name: 'Alice' }, { merge: true }).then(() => {}); nsDocRef.update({ count: 2 }).then(() => {}); nsDocRef.update('count', 3).then(() => {}); @@ -401,13 +399,15 @@ void nsLoadTask.then(() => {}); const nsNamed = nsFirestore.namedQuery('my-query'); void nsNamed; -nsFirestore.runTransaction(async (tx: FirebaseFirestoreTypes.Transaction) => { - const snap = await tx.get(nsDocRef); - if (snap.exists()) { - tx.update(nsDocRef, { count: ((snap.data() as { count?: number })?.count ?? 0) + 1 }); - } - return null; -}).then(() => {}); +nsFirestore + .runTransaction(async (tx: FirebaseFirestoreTypes.Transaction) => { + const snap = await tx.get(nsDocRef); + if (snap.exists()) { + tx.update(nsDocRef, { count: ((snap.data() as { count?: number })?.count ?? 0) + 1 }); + } + return null; + }) + .then(() => {}); // ----- Firestore instance: persistence and network ----- nsFirestore.clearPersistence().then(() => {}); @@ -452,13 +452,15 @@ const nsArrayRemove = firebase.firestore.FieldValue.arrayRemove(1); void nsArrayRemove; const nsIncrement = firebase.firestore.FieldValue.increment(1); -nsDocRef.set({ - name: 'x', - deleted: nsDelete, - ts: nsServerTs, - arr: nsArrayUnion, - cnt: nsIncrement, -}).then(() => {}); +nsDocRef + .set({ + name: 'x', + deleted: nsDelete, + ts: nsServerTs, + arr: nsArrayUnion, + cnt: nsIncrement, + }) + .then(() => {}); // ----- withConverter (namespaced) ----- interface User { @@ -481,7 +483,6 @@ nsDocWithConv.get().then((snap: FirebaseFirestoreTypes.DocumentSnapshot) = if (u) void [u.name, u.age]; }); - // ----- getFirestore ----- const modFirestore1 = getFirestore(); void modFirestore1.app.name; @@ -970,10 +971,7 @@ const pipelineUnion = pipelineDb .collection('cities/sf/restaurants') .where(field('type').equal('Chinese')) .union( - pipelineDb - .pipeline() - .collection('cities/ny/restaurants') - .where(field('type').equal('Italian')), + pipelineDb.pipeline().collection('cities/ny/restaurants').where(field('type').equal('Italian')), ) .where(field('rating').greaterThanOrEqual(4.5)) .sort(field('__name__').descending()); @@ -984,10 +982,7 @@ const pipelineWithTransforms = pipelineDb .collection('books') .where( pipelineOr( - pipelineAnd( - field('rating').greaterThan(4), - lessThan(field('price'), constant(10)), - ), + pipelineAnd(field('rating').greaterThan(4), lessThan(field('price'), constant(10))), field('genre').equal('Fantasy'), ), ) @@ -996,9 +991,7 @@ const pipelineWithTransforms = pipelineDb .select( field('fullTitle'), field('rating').greaterThan(4).as('isTopRated'), - arrayContainsAny(field('genre'), ['Fantasy', constant('Sci-Fi')]).as( - 'matchesGenre', - ), + arrayContainsAny(field('genre'), ['Fantasy', constant('Sci-Fi')]).as('matchesGenre'), ) .sort(Ordering.of(field('rating')).descending(), field('__name__').ascending()) .offset(1) @@ -1015,22 +1008,22 @@ const pipelineAggregateDistinct = pipelineDb pipelineAverage('population').as('populationAvg'), maximum('population').as('populationMax'), ], - groups: [ - field('country').as('country'), - toLower(field('state')).as('normalizedState'), - ], + groups: [field('country').as('country'), toLower(field('state')).as('normalizedState')], }) .where(field('populationTotal').greaterThan(1000)) .distinct(field('normalizedState'), 'country'); void pipelineAggregateDistinct; -const pipelineFindNearest = pipelineDb.pipeline().collection('cities').findNearest({ - field: 'embedding', - vectorValue: [1.5, 2.345], - distanceMeasure: 'COSINE', - distanceField: 'computedDistance', - limit: 10, -}); +const pipelineFindNearest = pipelineDb + .pipeline() + .collection('cities') + .findNearest({ + field: 'embedding', + vectorValue: [1.5, 2.345], + distanceMeasure: 'COSINE', + distanceField: 'computedDistance', + limit: 10, + }); void pipelineFindNearest; const pipelineSampleAndUnnest = pipelineDb @@ -1113,7 +1106,11 @@ const _cStr: Expression = constant('hello'); const _cBool: BooleanExpression = constant(true); const _cNull: Expression = constant(null); const _cUnknown: Expression = constant({ nested: true }); -void _cNum; void _cStr; void _cBool; void _cNull; void _cUnknown; +void _cNum; +void _cStr; +void _cBool; +void _cNull; +void _cUnknown; // ----- Comparison: standalone overloads ----- // greaterThan(Expression, Expression) | greaterThan(Expression, value) @@ -1536,11 +1533,9 @@ const pipelineComparisonOps = xDb ) .select( field('sku'), - conditional( - field('stock').greaterThan(0), - constant('in-stock'), - constant('out-of-stock'), - ).as('availability'), + conditional(field('stock').greaterThan(0), constant('in-stock'), constant('out-of-stock')).as( + 'availability', + ), isType(field('value'), 'string').as('isString'), logicalMaximum(field('bidA'), field('bidB')).as('topBid'), logicalMinimum(field('askA'), field('askB')).as('bottomAsk'), @@ -1595,10 +1590,7 @@ const pipelineStringOps = xDb stringContains(field('bio'), 'developer'), like('role', 'eng%'), regexContains(field('phone'), '^\\+1'), - xor( - field('isPublic').equal(true), - field('isVerified').equal(true), - ), + xor(field('isPublic').equal(true), field('isVerified').equal(true)), ), ) .addFields( @@ -1722,10 +1714,7 @@ const pipelineAllAggregates = xDb arrayAggDistinct(field('category')).as('distinctCategories'), arrayAggDistinct('category').as('distinctCategories2'), ], - groups: [ - field('country').as('country'), - toLower(field('state')).as('normalizedState'), - ], + groups: [field('country').as('country'), toLower(field('state')).as('normalizedState')], }); void pipelineAllAggregates; diff --git a/packages/firestore/e2e/helpers.js b/packages/firestore/e2e/helpers.js index 800e633670..c44aa4e156 100644 --- a/packages/firestore/e2e/helpers.js +++ b/packages/firestore/e2e/helpers.js @@ -43,9 +43,7 @@ exports.wipe = async function wipe(debug = false, databaseId = '(default)', retr if (!response.ok) { const body = await response.text(); - throw new Error( - `Firestore wipe failed: HTTP ${response.status} ${body.slice(0, 200)}`, - ); + throw new Error(`Firestore wipe failed: HTTP ${response.status} ${body.slice(0, 200)}`); } if (debug) { diff --git a/packages/in-app-messaging/lib/modular.ts b/packages/in-app-messaging/lib/modular.ts index f0174d67d8..2e2e63fefd 100644 --- a/packages/in-app-messaging/lib/modular.ts +++ b/packages/in-app-messaging/lib/modular.ts @@ -23,7 +23,9 @@ import { import type { InAppMessaging } from './types/in-app-messaging'; import type { InAppMessagingWithDeprecationArg } from './types/internal'; -function withModularDeprecationArg(inAppMessaging: InAppMessaging): InAppMessagingWithDeprecationArg { +function withModularDeprecationArg( + inAppMessaging: InAppMessaging, +): InAppMessagingWithDeprecationArg { return inAppMessaging as InAppMessagingWithDeprecationArg; } diff --git a/packages/installations/README.md b/packages/installations/README.md index 06f49e5e91..cee8142fb9 100644 --- a/packages/installations/README.md +++ b/packages/installations/README.md @@ -19,7 +19,7 @@ Follow on Facebook

----- +--- Entry point for Firebase installations. @@ -29,7 +29,6 @@ The Firebase installations service: - provides an auth token for a Firebase installation - provides a API to perform GDPR-compliant deletion of a Firebase installation. - [> Learn More](https://firebase.google.com/docs/projects/manage-installations) ## Installation @@ -42,14 +41,14 @@ yarn add @react-native-firebase/installations ## Documentation - - [Guides](https://rnfirebase.io/installations/usage/) - - [Reference](https://rnfirebase.io/reference/installations) +- [Guides](https://rnfirebase.io/installations/usage/) +- [Reference](https://rnfirebase.io/reference/installations) ## License - See [LICENSE](/LICENSE) ----- +---

@@ -58,4 +57,4 @@ yarn add @react-native-firebase/installations

----- +--- diff --git a/packages/messaging/README.md b/packages/messaging/README.md index e7dc3e770f..c90d6551cd 100644 --- a/packages/messaging/README.md +++ b/packages/messaging/README.md @@ -19,7 +19,7 @@ Follow on Facebook

----- +--- React Native Firebase provides native integration of Firebase Cloud Messaging (FCM) for both Android & iOS. FCM is a cost free service, allowing for server-device and device-device communication. @@ -42,6 +42,7 @@ yarn add @react-native-firebase/messaging - [Reference](https://rnfirebase.io/reference/messaging) ### Additional Topics + - [iOS Permissions](https://rnfirebase.io/messaging/ios-permissions) - [Notifications](https://rnfirebase.io/messaging/notifications) - [Server Integration](https://rnfirebase.io/messaging/server-integration) @@ -50,7 +51,7 @@ yarn add @react-native-firebase/messaging - See [LICENSE](/LICENSE) ----- +---

@@ -59,4 +60,4 @@ yarn add @react-native-firebase/messaging

----- +--- diff --git a/packages/phone-number-verification/lib/modular.ts b/packages/phone-number-verification/lib/modular.ts index 1c5874afd7..9abb6f729c 100644 --- a/packages/phone-number-verification/lib/modular.ts +++ b/packages/phone-number-verification/lib/modular.ts @@ -77,9 +77,7 @@ export function enableTestSession(token: string): Promise { * @returns Array of support results, one per SIM slot. * @see https://firebase.google.com/docs/phone-number-verification */ -export function getVerificationSupportInfo( - simSlot?: number, -): Promise { +export function getVerificationSupportInfo(simSlot?: number): Promise { if (simSlot !== undefined) { return getNativeModule().getVerificationSupportInfoForSimSlot(simSlot); } diff --git a/packages/vertexai/README.md b/packages/vertexai/README.md index d179e79e14..b825eedfa8 100644 --- a/packages/vertexai/README.md +++ b/packages/vertexai/README.md @@ -15,12 +15,12 @@ To start using the new SDK, import the `@react-native-firebase/ai` package and u ```javascript // BEFORE - using firebase/vertexai -import { initializeApp } from "firebase/app"; -import { getVertexAI, getGenerativeModel } from "firebase/vertexai"; // Remove this +import { initializeApp } from 'firebase/app'; +import { getVertexAI, getGenerativeModel } from 'firebase/vertexai'; // Remove this // AFTER - using firebase/ai -import { initializeApp } from "firebase/app"; -import { getAI, getGenerativeModel } from "firebase/ai"; // Add this +import { initializeApp } from 'firebase/app'; +import { getAI, getGenerativeModel } from 'firebase/ai'; // Add this ``` --- diff --git a/scripts/repro-android-build-flake.sh b/scripts/repro-android-build-flake.sh new file mode 100755 index 0000000000..f5a302de93 --- /dev/null +++ b/scripts/repro-android-build-flake.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Reproduce intermittent Android packageDebugAndroidTest / IncrementalSplitterRunnable +# failures under cold install + parallel host load (mirrors run-full-tests.sh phase 2–5). +# +# Usage: +# ./scripts/repro-android-build-flake.sh [attempts] +# ATTEMPTS=5 ./scripts/repro-android-build-flake.sh +# +# Logs: +# /tmp/rnfb-android-flake-repro.log full output +# /tmp/rnfb-android-flake-repro.summary pass/fail per attempt + +set -u + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +ATTEMPTS="${1:-${ATTEMPTS:-5}}" +LOG="${LOG:-/tmp/rnfb-android-flake-repro.log}" +SUMMARY="${SUMMARY:-/tmp/rnfb-android-flake-repro.summary}" + +: >"$LOG" +: >"$SUMMARY" + +log() { + echo "$1" | tee -a "$LOG" "$SUMMARY" +} + +# Plain rm only — do not call yarn build:clean before node_modules exists (rimraf lives there). +clean_workspace_artifacts() { + log " stopping gradle daemons..." + (cd tests/android && ./gradlew --stop >>"$LOG" 2>&1) || true + + log " removing android/ios/macos build dirs..." + rm -rf \ + tests/android/build \ + tests/android/app/build \ + tests/android/.gradle \ + tests/android/app/.cxx \ + tests/ios/build \ + tests/macos/build \ + tests/dist + + log " removing all node_modules..." + find . -name node_modules -type d -prune -exec rm -rf {} + 2>/dev/null || true +} + +cold_reset() { + log "=== COLD RESET $(date -Iseconds) ===" + clean_workspace_artifacts + + log " yarn install (codegen)..." + yarn install >>"$LOG" 2>&1 || return 1 + + log " gradlew clean (post-codegen)..." + (cd tests/android && ./gradlew clean --no-build-cache >>"$LOG" 2>&1) || true + + log " pod install (ios + macos)..." + (yarn tests:ios:pod:install >>"$LOG" 2>&1 & yarn tests:macos:pod:install >>"$LOG" 2>&1 & wait) || return 1 +} + +# Android build with maximum coldness; other jobs match run-full-tests.sh parallel block. +run_android_build_cold() { + ( + cd tests/android + ./gradlew-with-worker-cap.sh \ + assembleDebug assembleAndroidTest lintDebug \ + -DtestBuildType=debug \ + --warning-mode all \ + --stacktrace \ + --no-build-cache \ + --rerun-tasks + ) >>"$LOG" 2>&1 +} + +parallel_verify() { + log "=== PARALLEL VERIFY $(date -Iseconds) ===" + local pids=() + local failed=0 + + yarn tests:ios:build >>"$LOG" 2>&1 & pids+=($!) + yarn tests:macos:build >>"$LOG" 2>&1 & pids+=($!) + run_android_build_cold & pids+=($!) + yarn compare:types >>"$LOG" 2>&1 & pids+=($!) + yarn lint:js >>"$LOG" 2>&1 & pids+=($!) + yarn lint:ios:check >>"$LOG" 2>&1 & pids+=($!) + yarn lint:markdown >>"$LOG" 2>&1 & pids+=($!) + yarn lint:spellcheck >>"$LOG" 2>&1 & pids+=($!) + yarn tests:jest >>"$LOG" 2>&1 & pids+=($!) + + for pid in "${pids[@]}"; do + wait "$pid" || failed=1 + done + + return "$failed" +} + +extract_failure_hints() { + grep -nE \ + 'IncrementalSplitter|packageDebugAndroidTest FAILED|Caused by|NoSuchFile|OutOfMemory|BUILD FAILED|Command failed' \ + "$LOG" | tail -40 | tee -a "$SUMMARY" || true +} + +pass_count=0 +fail_count=0 + +for i in $(seq 1 "$ATTEMPTS"); do + log "" + log "=== ATTEMPT $i/$ATTEMPTS $(date -Iseconds) ===" + + if cold_reset && parallel_verify; then + log "ATTEMPT $i: PASS $(date -Iseconds)" + pass_count=$((pass_count + 1)) + else + log "ATTEMPT $i: FAIL $(date -Iseconds)" + fail_count=$((fail_count + 1)) + extract_failure_hints + fi +done + +log "" +log "=== SUMMARY: $pass_count passed, $fail_count failed (of $ATTEMPTS) ===" +log "Full log: $LOG" diff --git a/scripts/run-full-tests.sh b/scripts/run-full-tests.sh index bfc853e2b4..8c4be5359c 100755 --- a/scripts/run-full-tests.sh +++ b/scripts/run-full-tests.sh @@ -5,6 +5,7 @@ set -e # Create temporary directory for logs TMP_DIR=$(mktemp -d) +echo "Step logs directory: $TMP_DIR" # Clean up any stale metro bundler or firebase emulator processes function terminate_testing_processes() { @@ -37,8 +38,8 @@ run_yarn_script() { # Run the yarn command and redirect output to log file if ! yarn "$script_name" > "$log_file" 2>&1; then echo "Command failed: yarn $script_name" + echo "Full log preserved at: $log_file" cat "$log_file" - rm -f "$log_file" return 1 fi @@ -50,7 +51,10 @@ run_yarn_script() { run_yarn_scripts_parallel() { local scripts=("$@") local pids=() - local pid failed=0 + local script_names=() + local failed_scripts=() + local i=0 + local failed=0 ( trap 'kill 0' SIGINT @@ -58,12 +62,24 @@ run_yarn_scripts_parallel() { for script_name in "${scripts[@]}"; do run_yarn_script "$script_name" & pids+=($!) + script_names+=("$script_name") done for pid in "${pids[@]}"; do - wait "$pid" || failed=1 + if ! wait "$pid"; then + failed=1 + failed_scripts+=("${script_names[$i]}") + fi + i=$((i + 1)) done + if [ "$failed" -ne 0 ]; then + echo "Parallel step failed. Preserved logs in: $TMP_DIR" + for script_name in "${failed_scripts[@]}"; do + echo " - ${TMP_DIR}/${script_name}.log" + done + fi + exit "$failed" ) || return 1 } @@ -72,13 +88,13 @@ echo "Starting full test execution..." # 1. Dependency Installation echo "Installing dependencies..." -run_yarn_script "install" || { echo "yarn install failed"; exit 1; } +run_yarn_script "install" || { echo "yarn install failed. Logs preserved in: $TMP_DIR"; exit 1; } echo "Installing iOS and macOS pods in parallel..." run_yarn_scripts_parallel \ "tests:ios:pod:install" \ "tests:macos:pod:install" \ - || { echo "Pod install failed"; exit 1; } + || { echo "Pod install failed. Logs preserved in: $TMP_DIR"; exit 1; } # 2–5. Builds, typechecks, lint, and unit tests (all parallel) echo "Running builds, typechecks, lint, and unit tests in parallel..." @@ -92,7 +108,7 @@ run_yarn_scripts_parallel \ "lint:markdown" \ "lint:spellcheck" \ "tests:jest" \ - || { echo "Parallel verification failed"; exit 1; } + || { echo "Parallel verification failed. Logs preserved in: $TMP_DIR"; exit 1; } # 6. E2E Tests with Flakiness Tolerance echo "Running E2E tests..." @@ -114,10 +130,13 @@ sleep 30 # Run E2E tests - 3 chances to succeed for flake tolerance for flavor in "ios" "android" "macos"; do for i in {1..3}; do - echo "Running $flavor E2E test run attempt $i..." - if ! yarn tests:"$flavor":test; then + e2e_log="${TMP_DIR}/tests:${flavor}:test.attempt${i}.log" + echo "Running $flavor E2E test run attempt $i... (log: $e2e_log)" + if ! yarn tests:"$flavor":test > "$e2e_log" 2>&1; then + echo "E2E attempt failed. Full log preserved at: $e2e_log" + cat "$e2e_log" if [ $i -eq 3 ]; then - echo "$flavor E2E test failed all $i attempts."; + echo "$flavor E2E test failed all $i attempts. Logs preserved in: $TMP_DIR" terminate_testing_processes exit 1; fi diff --git a/tests/.babelrc b/tests/.babelrc index cef28c9b85..8f47e44072 100644 --- a/tests/.babelrc +++ b/tests/.babelrc @@ -1,6 +1,12 @@ { "presets": ["module:@react-native/babel-preset"], "plugins": [ + [ + "transform-inline-environment-variables", + { + "include": ["CI"] + } + ], [ "istanbul", { diff --git a/tests/.detoxrc.js b/tests/.detoxrc.js index 1bbbbf02f2..be9388399e 100644 --- a/tests/.detoxrc.js +++ b/tests/.detoxrc.js @@ -27,7 +27,7 @@ module.exports = { binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', // keep in sync with android.debug.windows below, except gradlew vs gradlew.bat build: - 'cd android && ./gradlew assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all && cd ..', + 'cd android && ./gradlew-with-worker-cap.sh assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all --stacktrace && cd ..', reversePorts: [8080, 8081, 8090, 9000, 9099, 9199], }, 'android.debug.windows': { @@ -35,7 +35,7 @@ module.exports = { binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', // android.debug.windows only exists to use .bat script vs shell here: build: - 'cd android && .\\gradlew.bat assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all && cd ..', + 'cd android && .\\gradlew-with-worker-cap.bat assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all --stacktrace && cd ..', reversePorts: [8080, 8081, 8090, 9000, 9099, 9199], }, 'android.release': { diff --git a/tests/android/gradle.properties b/tests/android/gradle.properties index 0742458a09..2b6458b95d 100644 --- a/tests/android/gradle.properties +++ b/tests/android/gradle.properties @@ -11,7 +11,7 @@ org.gradle.daemon=true org.gradle.caching=true org.gradle.parallel=true org.gradle.configureondemand=true -org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx5120M -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/tests/android/gradlew-with-worker-cap.bat b/tests/android/gradlew-with-worker-cap.bat new file mode 100644 index 0000000000..101a2717a1 --- /dev/null +++ b/tests/android/gradlew-with-worker-cap.bat @@ -0,0 +1,25 @@ +@echo off +setlocal EnableDelayedExpansion + +if not defined GRADLE_WORKER_CAP set "GRADLE_WORKER_CAP=6" + +set "CPUS=" +for /f %%i in ('wmic cpu get NumberOfCores /value ^| find "="') do ( + for /f "tokens=2 delims==" %%j in ("%%i") do set "CPUS=%%j" +) + +if not defined CPUS ( + if defined NUMBER_OF_PROCESSORS ( + set /a CPUS=NUMBER_OF_PROCESSORS / 2 + ) else ( + set "CPUS=4" + ) +) + +if !CPUS! LSS 1 set "CPUS=1" + +set "WORKERS=!CPUS!" +if !WORKERS! GTR %GRADLE_WORKER_CAP% set "WORKERS=%GRADLE_WORKER_CAP%" + +echo [gradlew-with-worker-cap] cpus=!CPUS! cap=%GRADLE_WORKER_CAP% -^> --max-workers=!WORKERS! 1>&2 +gradlew.bat --max-workers=!WORKERS! %* diff --git a/tests/android/gradlew-with-worker-cap.sh b/tests/android/gradlew-with-worker-cap.sh new file mode 100755 index 0000000000..50d993776d --- /dev/null +++ b/tests/android/gradlew-with-worker-cap.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +WORKER_CAP="${GRADLE_WORKER_CAP:-6}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +resolve_physical_cpus() { + local cpus + + case "$(uname -s)" in + Darwin) + if cpus="$(sysctl -n hw.physicalcpu 2>/dev/null)" && [[ "$cpus" =~ ^[0-9]+$ ]] && (( cpus >= 1 )); then + echo "$cpus" + return + fi + ;; + Linux) + if command -v lscpu >/dev/null 2>&1; then + cpus="$(lscpu -p=CORE 2>/dev/null | grep -Ev '^#' | sort -u | wc -l | tr -d ' ')" + if [[ "$cpus" =~ ^[0-9]+$ ]] && (( cpus >= 1 )); then + echo "$cpus" + return + fi + local sockets cores_per_socket + sockets="$(lscpu 2>/dev/null | awk '/^Socket\(s\)/ {print $2; exit}')" + cores_per_socket="$(lscpu 2>/dev/null | awk '/^Core\(s\) per socket/ {print $4; exit}')" + if [[ "$sockets" =~ ^[0-9]+$ && "$cores_per_socket" =~ ^[0-9]+$ ]] && (( sockets >= 1 && cores_per_socket >= 1 )); then + echo $(( sockets * cores_per_socket )) + return + fi + fi + if [[ -r /proc/cpuinfo ]]; then + cpus="$(awk '/^physical id/ {pid=$4} /^core id/ {cid=$4; k=pid","cid; if (!seen[k]++) n++} END {print n+0}' /proc/cpuinfo)" + if [[ "$cpus" =~ ^[0-9]+$ ]] && (( cpus >= 1 )); then + echo "$cpus" + return + fi + fi + ;; + esac + + # Fallback: assume SMT — use half of logical CPUs + local logical=4 + if command -v getconf >/dev/null 2>&1; then + logical="$(getconf _NPROCESSORS_ONLN)" + elif [[ -r /proc/cpuinfo ]]; then + logical="$(grep -c ^processor /proc/cpuinfo)" + fi + echo $(( logical / 2 > 0 ? logical / 2 : 1 )) +} + +cpus="$(resolve_physical_cpus)" +workers=$(( cpus < WORKER_CAP ? cpus : WORKER_CAP )) +(( workers >= 1 )) || workers=1 + +echo "[gradlew-with-worker-cap] cpus=$cpus cap=$WORKER_CAP -> --max-workers=$workers" >&2 +exec ./gradlew --max-workers="$workers" "$@" diff --git a/tests/e2e/firebase.test.js b/tests/e2e/firebase.test.js index 991519a9c9..a7dbced9f4 100644 --- a/tests/e2e/firebase.test.js +++ b/tests/e2e/firebase.test.js @@ -25,12 +25,19 @@ const JET_REMOTE_PORT = parseInt(process.env.JET_REMOTE_PORT || '8090', 10); const METRO_PORT = parseInt(process.env.JET_METRO_PORT || process.env.RCT_METRO_PORT || '8081', 10); const LAUNCH_APP_TIMEOUT_MS = parseInt(process.env.RNFB_LAUNCH_APP_TIMEOUT_MS || '180000', 10); const LAUNCH_APP_MAX_ATTEMPTS = parseInt(process.env.RNFB_LAUNCH_APP_MAX_ATTEMPTS || '2', 10); +const REBOOT_IOS_SIMULATOR_TIMEOUT_MS = parseInt( + process.env.RNFB_REBOOT_IOS_SIMULATOR_TIMEOUT_MS || String(12 * 60 * 1000), + 10, +); const JET_RETRYABLE_WS_RE = /\[jet-ws\] RETRYABLE_DISCONNECT code=(1006|1001)\b/; const JET_RECONNECT_RECOVERED_RE = /\[jet-ws\] reconnect_recovered code=(1006|1001)\b/; const JET_SERVER_NOT_RUNNING_RE = /server wasn't running/i; const JET_COVERAGE_LOST_RE = /Coverage summary:[\s\S]*?Unknown% \( 0\/0 \)/; const RETRYABLE_LAUNCH_RE = - /launchApp timed out|RCTJavaScriptDidFailToLoad|packager-probe|Metro not responding/i; + /launchApp timed out|RCTJavaScriptDidFailToLoad|packager-probe|Metro not responding|Unknown application display identifier|Simulator device failed to launch/i; +const PORT_CLOSED_ERROR_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'EPIPE']); + +let cachedUsesLiveMetro; function resolveDetoxConfigurationName() { if (process.env.DETOX_CONFIGURATION) { @@ -55,6 +62,10 @@ function resolveAppBinaryPath() { } function usesLiveMetro() { + if (cachedUsesLiveMetro !== undefined) { + return cachedUsesLiveMetro; + } + const configName = resolveDetoxConfigurationName(); if (/debug/i.test(configName)) { return true; @@ -74,6 +85,40 @@ function usesLiveMetro() { return false; } +function cacheUsesLiveMetro() { + cachedUsesLiveMetro = usesLiveMetro(); + console.log(`[rnfb-e2e] cached usesLiveMetro=${cachedUsesLiveMetro}`); +} + +function rebootIosSimulator(testsDir) { + return new Promise((resolve, reject) => { + const repoRoot = path.resolve(testsDir, '..'); + const bootScript = path.join(repoRoot, '.github/workflows/scripts/boot-simulator.sh'); + console.warn(`[rnfb-e2e] Rebooting iOS simulator via ${bootScript}`); + const child = spawn('bash', [bootScript], { + cwd: repoRoot, + stdio: 'inherit', + }); + const timer = setTimeout(() => { + child.kill('SIGTERM'); + reject(new Error(`boot-simulator.sh timed out after ${REBOOT_IOS_SIMULATOR_TIMEOUT_MS}ms`)); + }, REBOOT_IOS_SIMULATOR_TIMEOUT_MS); + + child.on('close', code => { + clearTimeout(timer); + if (code === 0) { + resolve(); + return; + } + reject(new Error(`boot-simulator.sh failed with code ${code}`)); + }); + child.on('error', err => { + clearTimeout(timer); + reject(err); + }); + }); +} + function waitForTcpPort(port, host = '127.0.0.1', timeoutMs = 120000) { const start = Date.now(); @@ -99,6 +144,49 @@ function waitForTcpPort(port, host = '127.0.0.1', timeoutMs = 120000) { }); } +function waitForTcpPortClosed(port, host = '127.0.0.1', timeoutMs = 120000) { + const start = Date.now(); + let probes = 0; + + return new Promise((resolve, reject) => { + const probe = () => { + if (Date.now() - start > timeoutMs) { + reject( + new Error( + `Timed out waiting for ${host}:${port} to close after ${timeoutMs}ms (probes=${probes})`, + ), + ); + return; + } + + probes += 1; + const socket = net.connect(port, host); + socket.once('connect', () => { + socket.end(); + setTimeout(probe, 250); + }); + socket.once('error', err => { + socket.destroy(); + const elapsedMs = Date.now() - start; + const code = err?.code || 'UNKNOWN'; + if (PORT_CLOSED_ERROR_CODES.has(code)) { + console.log( + `[rnfb-e2e] port ${host}:${port} closed (code=${code}, elapsed=${elapsedMs}ms, probes=${probes})`, + ); + resolve(); + return; + } + console.warn( + `[rnfb-e2e] port-probe non-close error code=${code} host=${host} port=${port} probe=${probes}`, + ); + setTimeout(probe, 250); + }); + }; + + probe(); + }); +} + function isRetryableJetDisconnect(output) { return JET_RETRYABLE_WS_RE.test(output); } @@ -240,7 +328,7 @@ function runJetE2eAttempt(attempt) { process.stderr.write(text); }); - return new Promise(async (resolve, reject) => { + const exitPromise = new Promise((resolve, reject) => { jetProcess.on('error', err => { err.jetOutput = output; reject(err); @@ -255,30 +343,37 @@ function runJetE2eAttempt(attempt) { } resolve({ output }); }); + }); - try { - console.log(`[rnfb-e2e] Jet attempt ${attempt}: waiting for port ${JET_REMOTE_PORT}`); - await waitForTcpPort(JET_REMOTE_PORT); - if (usesLiveMetro()) { - console.log(`[rnfb-e2e] Jet attempt ${attempt}: waiting for Metro on port ${METRO_PORT}`); - await waitForMetro(METRO_PORT); - } else { - console.log( - `[rnfb-e2e] Jet attempt ${attempt}: skipping Metro wait (configuration=${resolveDetoxConfigurationName() || 'unknown'}, binary=${resolveAppBinaryPath() || 'unknown'})`, - ); - } - console.log(`[rnfb-e2e] Jet attempt ${attempt}: launching app`); - await launchAppWithRetry({ - detoxURLBlacklistRegex: `.*`, - // Avoid sync/idling blocking the main queue while Detox WS login is pending. - detoxEnableSynchronization: 'NO', - }); - } catch (err) { - jetProcess.kill(); - err.jetOutput = output; - reject(err); + const orchestrate = async () => { + console.log(`[rnfb-e2e] Jet attempt ${attempt}: waiting for port ${JET_REMOTE_PORT}`); + await waitForTcpPort(JET_REMOTE_PORT); + if (usesLiveMetro()) { + console.log(`[rnfb-e2e] Jet attempt ${attempt}: waiting for Metro on port ${METRO_PORT}`); + await waitForMetro(METRO_PORT); + } else { + console.log( + `[rnfb-e2e] Jet attempt ${attempt}: skipping Metro wait (configuration=${resolveDetoxConfigurationName() || 'unknown'}, binary=${resolveAppBinaryPath() || 'unknown'})`, + ); } - }); + console.log(`[rnfb-e2e] Jet attempt ${attempt}: launching app`); + await launchAppWithRetry({ + detoxURLBlacklistRegex: `.*`, + // Avoid sync/idling blocking the main queue while Detox WS login is pending. + detoxEnableSynchronization: 'NO', + }); + }; + + return Promise.race([ + orchestrate() + .then(() => exitPromise) + .catch(err => { + jetProcess.kill(); + err.jetOutput = output; + throw err; + }), + exitPromise, + ]); } describe('Jet Tests', function () { @@ -289,6 +384,8 @@ describe('Jet Tests', function () { const deviceId = detox.device.id; const testsDir = path.resolve(__dirname, '..'); + cacheUsesLiveMetro(); + let lastFailure; for (let attempt = 1; attempt <= 2; attempt++) { @@ -304,10 +401,18 @@ describe('Jet Tests', function () { } else if (isRetryableLaunchFailure(lastFailure)) { console.warn('[rnfb-e2e] Retrying after Metro/bundle load launch failure'); } - try { - await device.terminateApp(); - } catch (_) { - // No-op + console.log( + `[rnfb-e2e] Jet attempt ${attempt}: waiting for port ${JET_REMOTE_PORT} to close before retry`, + ); + await waitForTcpPortClosed(JET_REMOTE_PORT); + if (platform === 'ios' && process.platform === 'darwin') { + await rebootIosSimulator(testsDir); + } else { + try { + await device.terminateApp(); + } catch (_) { + // No-op + } } } diff --git a/tests/globals.js b/tests/globals.js index 948e359261..d4d14e15fc 100644 --- a/tests/globals.js +++ b/tests/globals.js @@ -493,6 +493,6 @@ global.jet = { }; // some tests flake in CI but we still run them locally -global.isCI = process.env.CI === true; +global.isCI = process.env.CI === 'true' || process.env.CI === true; // Used to tell our internals that we are running tests. globalThis.RNFBTest = true; diff --git a/tests/package.json b/tests/package.json index 1851c3d855..d3ee7d7697 100644 --- a/tests/package.json +++ b/tests/package.json @@ -45,6 +45,7 @@ "@react-native/metro-config": "^0.78.3", "assert": "^2.1.0", "axios": "^1.15.2", + "babel-plugin-transform-inline-environment-variables": "^0.4.4", "cpy-cli": "^7.0.0", "detox": "patch:detox@npm%3A20.51.0#~/.yarn/patches/detox-npm-20.51.0-3e13b6e309.patch", "firebase": "^12.13.0", diff --git a/yarn.lock b/yarn.lock index ed8cf17af5..e14ff1b12c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8765,6 +8765,13 @@ __metadata: languageName: node linkType: hard +"babel-plugin-transform-inline-environment-variables@npm:^0.4.4": + version: 0.4.4 + resolution: "babel-plugin-transform-inline-environment-variables@npm:0.4.4" + checksum: 10/fa361287411301237fd8ce332aff4f8e8ccb8db30e87a2ddc7224c8bf7cd792eda47aca24dc2e09e70bce4c027bc8cbe22f4999056be37a25d2472945df21ef5 + languageName: node + linkType: hard + "babel-preset-current-node-syntax@npm:^1.0.0, babel-preset-current-node-syntax@npm:^1.2.0": version: 1.2.0 resolution: "babel-preset-current-node-syntax@npm:1.2.0" @@ -19554,13 +19561,13 @@ __metadata: "mocha-remote-server@patch:mocha-remote-server@npm%3A1.13.2#~/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch": version: 1.13.2 - resolution: "mocha-remote-server@patch:mocha-remote-server@npm%3A1.13.2#~/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch::version=1.13.2&hash=5b470d" + resolution: "mocha-remote-server@patch:mocha-remote-server@npm%3A1.13.2#~/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch::version=1.13.2&hash=9f1e51" dependencies: debug: "npm:^4.3.4" flatted: "npm:^3.3.1" mocha-remote-common: "npm:1.13.2" ws: "npm:^8.17.1" - checksum: 10/c5861226362636fac484237e3653e6442c88a7b0b12b240ab33482961b09b5c2a1a54190c03237b2c2274616be6fe7e5c8484b355a00bb28ee401cb7b81351b6 + checksum: 10/026256e831efc672c04312c7083927de218b2cf911eb7710abbc3c9ccb76730241eefc5bfb1f93873120244c80cb29cbd447a7edfa99bc2ce8fdc937ddc15c8c languageName: node linkType: hard @@ -22318,6 +22325,7 @@ __metadata: "@react-native/metro-config": "npm:^0.78.3" assert: "npm:^2.1.0" axios: "npm:^1.15.2" + babel-plugin-transform-inline-environment-variables: "npm:^0.4.4" cpy-cli: "npm:^7.0.0" detox: "patch:detox@npm%3A20.51.0#~/.yarn/patches/detox-npm-20.51.0-3e13b6e309.patch" firebase: "npm:^12.13.0"