From 1c5ce8092452399a21513e631be88974b44c1214 Mon Sep 17 00:00:00 2001 From: Simon Holmes Date: Fri, 1 May 2026 10:09:06 +0100 Subject: [PATCH] fix: add explicit defaults for all ReactNativeFeatureFlags (#24) The Proxy-only mock left every flag except isLayoutAnimationEnabled absent as own-properties, so named imports / Object.keys / spreads could miss enableNativeCSSParsing and other flags. Enumerate every flag from RN 0.83 with its real default and keep the Proxy fallback for future flags. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/vitest-react-native/src/setup.ts | 110 +++++++++++++++++++++- test/__tests__/FeatureFlags.spec.tsx | 37 +++++--- 2 files changed, 133 insertions(+), 14 deletions(-) diff --git a/packages/vitest-react-native/src/setup.ts b/packages/vitest-react-native/src/setup.ts index 9fe0a3e..66e2569 100644 --- a/packages/vitest-react-native/src/setup.ts +++ b/packages/vitest-react-native/src/setup.ts @@ -302,11 +302,117 @@ mock( }` ); -// Feature Flags - use Proxy to auto-handle any current or future flag +// Feature Flags - explicit defaults for every known flag (RN 0.83) so named +// imports / Object.keys / spreads see the full surface. Proxy fallback covers +// flags added in future RN releases without needing a setup.ts update. mock( 'react-native/src/private/featureflags/ReactNativeFeatureFlags', () => `(() => { - const defaults = { isLayoutAnimationEnabled: () => true }; + const f = (v) => () => v; + const defaults = { + animatedShouldDebounceQueueFlush: f(false), + animatedShouldUseSingleOp: f(false), + cdpInteractionMetricsEnabled: f(false), + commonTestFlag: f(false), + commonTestFlagWithoutNativeImplementation: f(false), + configurePressabilityDuringInsertion: f(false), + cxxNativeAnimatedEnabled: f(false), + cxxNativeAnimatedRemoveJsSync: f(false), + deferFlatListFocusChangeRenderUpdate: f(false), + disableEarlyViewCommandExecution: f(false), + disableFabricCommitInCXXAnimated: f(false), + disableMaintainVisibleContentPosition: f(false), + disableMountItemReorderingAndroid: f(false), + disableOldAndroidAttachmentMetricsWorkarounds: f(true), + disableTextLayoutManagerCacheAndroid: f(false), + enableAccessibilityOrder: f(false), + enableAccumulatedUpdatesInRawPropsAndroid: f(false), + enableAndroidLinearText: f(false), + enableAndroidTextMeasurementOptimizations: f(false), + enableBridgelessArchitecture: f(false), + enableCppPropsIteratorSetter: f(false), + enableCustomFocusSearchOnClippedElementsAndroid: f(true), + enableDestroyShadowTreeRevisionAsync: f(false), + enableDoubleMeasurementFixAndroid: f(false), + enableEagerMainQueueModulesOnIOS: f(false), + enableEagerRootViewAttachment: f(false), + enableFabricLogs: f(false), + enableFabricRenderer: f(false), + enableFontScaleChangesUpdatingLayout: f(true), + enableImagePrefetchingAndroid: f(false), + enableImagePrefetchingOnUiThreadAndroid: f(false), + enableImmediateUpdateModeForContentOffsetChanges: f(false), + enableImperativeFocus: f(false), + enableInteropViewManagerClassLookUpOptimizationIOS: f(false), + enableIntersectionObserverByDefault: f(false), + enableIOSTextBaselineOffsetPerLine: f(false), + enableIOSViewClipToPaddingBox: f(false), + enableKeyEvents: f(false), + enableLayoutAnimationsOnAndroid: f(false), + enableLayoutAnimationsOnIOS: f(true), + enableMainQueueCoordinatorOnIOS: f(false), + enableModuleArgumentNSNullConversionIOS: f(false), + enableNativeCSSParsing: f(false), + enableNetworkEventReporting: f(true), + enablePreparedTextLayout: f(false), + enablePropsUpdateReconciliationAndroid: f(false), + enableResourceTimingAPI: f(true), + enableSwiftUIBasedFilters: f(false), + enableViewCulling: f(false), + enableViewRecycling: f(false), + enableViewRecyclingForImage: f(true), + enableViewRecyclingForScrollView: f(false), + enableViewRecyclingForText: f(true), + enableViewRecyclingForView: f(true), + enableVirtualViewClippingWithoutScrollViewClipping: f(true), + enableVirtualViewContainerStateExperimental: f(false), + enableVirtualViewDebugFeatures: f(false), + enableVirtualViewExperimental: f(false), + enableVirtualViewRenderState: f(true), + enableVirtualViewWindowFocusDetection: f(false), + enableWebPerformanceAPIsByDefault: f(true), + fixMappingOfEventPrioritiesBetweenFabricAndReact: f(false), + fixVirtualizeListCollapseWindowSize: f(false), + fuseboxAssertSingleHostState: f(true), + fuseboxEnabledRelease: f(false), + fuseboxNetworkInspectionEnabled: f(true), + hideOffscreenVirtualViewsOnIOS: f(false), + isLayoutAnimationEnabled: f(true), + jsOnlyTestFlag: f(false), + overrideBySynchronousMountPropsAtMountingAndroid: f(false), + perfIssuesEnabled: f(false), + perfMonitorV2Enabled: f(false), + preparedTextCacheSize: f(200), + preventShadowTreeCommitExhaustion: f(false), + reduceDefaultPropsInImage: f(false), + reduceDefaultPropsInText: f(false), + shouldPressibilityUseW3CPointerEventsForHover: f(false), + shouldTriggerResponderTransferOnScrollAndroid: f(false), + shouldUseAnimatedObjectForTransform: f(false), + shouldUseRemoveClippedSubviewsAsDefaultOnIOS: f(false), + shouldUseSetNativePropsInFabric: f(true), + skipActivityIdentityAssertionOnHostPause: f(false), + sweepActiveTouchOnChildNativeGesturesAndroid: f(true), + traceTurboModulePromiseRejectionsOnAndroid: f(false), + updateRuntimeShadowNodeReferencesOnCommit: f(false), + useAlwaysAvailableJSErrorHandling: f(false), + useFabricInterop: f(true), + useNativeEqualsInNativeReadableArrayAndroid: f(true), + useNativeTransformHelperAndroid: f(true), + useNativeViewConfigsInBridgelessMode: f(false), + useOptimizedEventBatchingOnAndroid: f(false), + useRawPropsJsiValue: f(true), + useShadowNodeStateOnClone: f(false), + useSharedAnimatedBackend: f(false), + useTraitHiddenOnAndroid: f(false), + useTurboModuleInterop: f(false), + useTurboModules: f(false), + viewCullingOutsetRatio: f(0), + virtualViewActivityBehavior: f("no-activity"), + virtualViewHysteresisRatio: f(0), + virtualViewPrerenderRatio: f(5), + override: () => {}, + }; return new Proxy(defaults, { get: (target, prop) => { if (prop in target) return target[prop]; diff --git a/test/__tests__/FeatureFlags.spec.tsx b/test/__tests__/FeatureFlags.spec.tsx index 412cc6a..45973ee 100644 --- a/test/__tests__/FeatureFlags.spec.tsx +++ b/test/__tests__/FeatureFlags.spec.tsx @@ -1,33 +1,46 @@ import { test, expect, describe } from 'vitest'; /** - * Issue #13: ReactNativeFeatureFlags.enableNativeCSSParsing is not a function + * Issues #13 and #24: ReactNativeFeatureFlags missing functions * - * Verifies that the feature flags mock handles any current or future flag - * via a Proxy, so new flags don't require manual updates to setup.ts. + * Every flag from the current RN release is an explicit own-property so named + * imports, Object.keys, and spreads work. The Proxy fallback keeps unknown / + * future flags returning () => false without a setup.ts update. */ -describe('ReactNativeFeatureFlags (issue #13)', () => { - test('known flags are callable', () => { +describe('ReactNativeFeatureFlags', () => { + test('known boolean flags are callable with correct defaults', () => { const flags = require('react-native/src/private/featureflags/ReactNativeFeatureFlags'); - expect(typeof flags.enableNativeCSSParsing).toBe('function'); expect(flags.enableNativeCSSParsing()).toBe(false); + expect(flags.isLayoutAnimationEnabled()).toBe(true); + expect(flags.enableLayoutAnimationsOnIOS()).toBe(true); + expect(flags.enableFabricRenderer()).toBe(false); + expect(flags.useTurboModules()).toBe(false); }); - test('isLayoutAnimationEnabled defaults to true', () => { + test('non-boolean flags return correct default types', () => { const flags = require('react-native/src/private/featureflags/ReactNativeFeatureFlags'); - expect(flags.isLayoutAnimationEnabled()).toBe(true); + expect(flags.preparedTextCacheSize()).toBe(200); + expect(flags.virtualViewPrerenderRatio()).toBe(5); + expect(flags.virtualViewActivityBehavior()).toBe('no-activity'); + }); + + test('flags are own properties (Object.keys / spread / for...in)', () => { + const flags = require('react-native/src/private/featureflags/ReactNativeFeatureFlags'); + const keys = Object.keys(flags); + expect(keys).toContain('enableNativeCSSParsing'); + expect(keys).toContain('isLayoutAnimationEnabled'); + expect(keys.length).toBeGreaterThan(50); + const spread = { ...flags }; + expect(typeof spread.enableNativeCSSParsing).toBe('function'); }); test('unknown/future flags return () => false automatically', () => { const flags = require('react-native/src/private/featureflags/ReactNativeFeatureFlags'); - // Any arbitrary flag name should work without hardcoding - expect(typeof flags.someFutureFlag).toBe('function'); expect(flags.someFutureFlag()).toBe(false); - expect(typeof flags.anotherNewFeature).toBe('function'); expect(flags.anotherNewFeature()).toBe(false); }); - test('turbo module proxy also handles unknown flags', () => { + test('turbo module proxy handles any flag name', () => { const proxy = (globalThis as Record).__turboModuleProxy as ( name: string, ) => Record | null;