diff --git a/.github/workflows/e2e-v2.yml b/.github/workflows/e2e-v2.yml index 2b6d9480c6..1753738947 100644 --- a/.github/workflows/e2e-v2.yml +++ b/.github/workflows/e2e-v2.yml @@ -164,7 +164,7 @@ jobs: strategy: fail-fast: false # keeps matrix running if one fails matrix: - rn-version: ['0.65.3', '0.81.0'] + rn-version: ['0.69.12', '0.81.0'] rn-architecture: ['legacy', 'new'] platform: ['android', 'ios'] build-type: ['production'] @@ -176,7 +176,7 @@ jobs: xcode-version: '16.2' runs-on: macos-14 - platform: ios - rn-version: '0.65.3' + rn-version: '0.69.12' xcode-version: '14.2' runs-on: macos-13 - platform: android @@ -186,13 +186,13 @@ jobs: - rn-version: '0.81.0' engine: 'jsc' # exclude all rn versions lower than 0.70.0 for new architecture - - rn-version: '0.65.3' + - rn-version: '0.69.12' rn-architecture: 'new' # exlude old rn version for use frameworks builds (to minimalize the matrix) - - rn-version: '0.65.3' + - rn-version: '0.69.12' platform: 'ios' ios-use-frameworks: 'static' - - rn-version: '0.65.3' + - rn-version: '0.69.12' platform: 'ios' ios-use-frameworks: 'dynamic' # use frameworks is ios only feature @@ -238,7 +238,7 @@ jobs: - uses: actions/setup-java@v5 with: - java-version: ${{ matrix.rn-version == '0.65.3' && '11' || '17' }} + java-version: ${{ matrix.rn-version == '0.69.12' && '11' || '17' }} distribution: 'adopt' - name: Gradle cache @@ -262,7 +262,7 @@ jobs: # to avoid issues with the old node version - run: corepack disable - uses: actions/setup-node@v5 - if: ${{ matrix.rn-version == '0.65.3' }} + if: ${{ matrix.rn-version == '0.69.12' }} with: package-manager-cache: false node-version: 16 @@ -304,7 +304,7 @@ jobs: strategy: fail-fast: false # keeps matrix running if one fails matrix: - rn-version: ['0.65.3', '0.81.0'] + rn-version: ['0.69.12', '0.81.0'] rn-architecture: ['legacy', 'new'] platform: ['android', 'ios'] build-type: ['production'] @@ -315,16 +315,16 @@ jobs: rn-version: '0.81.0' runs-on: macos-14 - platform: ios - rn-version: '0.65.3' + rn-version: '0.69.12' runs-on: macos-14 - platform: android runs-on: ubuntu-latest exclude: # exclude all rn versions lower than 0.70.0 for new architecture - - rn-version: '0.65.3' + - rn-version: '0.69.12' rn-architecture: 'new' # e2e test only the default combinations - - rn-version: '0.65.3' + - rn-version: '0.69.12' engine: 'hermes' - rn-version: '0.81.0' engine: 'jsc' diff --git a/dev-packages/e2e-tests/cli.mjs b/dev-packages/e2e-tests/cli.mjs index 383391a079..da528c5429 100755 --- a/dev-packages/e2e-tests/cli.mjs +++ b/dev-packages/e2e-tests/cli.mjs @@ -70,7 +70,7 @@ const sentryAuthToken = env.SENTRY_AUTH_TOKEN; function runCodegenIfNeeded(rnVersion, platform, appDir) { const versionNumber = parseFloat(rnVersion.replace(/[^\d.]/g, '')); - const shouldRunCodegen = platform === 'android' && versionNumber >= 0.80; + const shouldRunCodegen = platform === 'android' && versionNumber >= 0.69; if (shouldRunCodegen) { console.log(`Running codegen for React Native ${rnVersion}...`); @@ -105,14 +105,14 @@ if (actions.includes('create')) { // Only apply the package.json patch for newer RN versions const versionNumber = parseFloat(RNVersion.replace(/[^\d.]/g, '')); - if (versionNumber >= 0.80) { + if (versionNumber >= 0.69) { execSync(`${patchScriptsDir}/rn.patch.package.json.js --path package.json`, { stdio: 'inherit', cwd: appDir, env: env, }); } else { - console.log(`Skipping rn.patch.package.json.js for RN ${RNVersion} (< 0.80)`); + console.log(`Skipping rn.patch.package.json.js for RN ${RNVersion} (< 0.69)`); } // Install dependencies diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 3548e17a6f..357737f3a5 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -34,7 +34,7 @@ Pod::Spec.new do |s| s.homepage = "https://github.com/getsentry/sentry-react-native" s.source = { :git => 'https://github.com/getsentry/sentry-react-native.git', :tag => "#{s.version}"} - s.ios.deployment_target = "11.0" + s.ios.deployment_target = "12.0" s.osx.deployment_target = "10.13" s.tvos.deployment_target = "11.0" s.visionos.deployment_target = "1.0" if s.respond_to?(:visionos) diff --git a/packages/core/package.json b/packages/core/package.json index e69d336447..ae307527fb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -76,18 +76,19 @@ "@sentry/types": "10.17.0" }, "devDependencies": { - "@babel/core": "^7.25.2", + "@babel/core": "^7.26.7", "@expo/metro-config": "~0.20.0", "@mswjs/interceptors": "^0.25.15", - "@react-native/babel-preset": "0.77.1", + "@react-native/babel-preset": "0.80.0", "@sentry-internal/eslint-config-sdk": "10.17.0", "@sentry-internal/eslint-plugin-sdk": "10.17.0", "@sentry-internal/typescript": "10.17.0", "@sentry/wizard": "6.5.0", - "@testing-library/react-native": "^12.7.2", + "@testing-library/react-native": "^13.2.2", "@types/jest": "^29.5.13", "@types/node": "^20.9.3", - "@types/react": "^18.2.64", + "@types/react": "^19.1.0", + "@types/react-test-renderer": "^19.1.0", "@types/uglify-js": "^3.17.2", "@types/uuid": "^9.0.4", "@types/xmlhttprequest": "^1.8.2", @@ -102,14 +103,15 @@ "eslint-plugin-react-native": "^3.8.1", "expo": "^53.0.0", "expo-module-scripts": "3.1.0", - "jest": "^29.6.2", + "jest": "^29.6.3", "jest-environment-jsdom": "^29.6.2", "jest-extended": "^4.0.2", "madge": "^6.1.0", "metro": "0.83.1", "prettier": "^2.0.5", - "react": "18.3.1", - "react-native": "0.77.1", + "react": "19.1.0", + "react-native": "0.80.1", + "react-test-renderer": "19.1.0", "rimraf": "^4.1.1", "ts-jest": "^29.3.1", "typescript": "4.9.5", diff --git a/packages/core/src/js/RNSentryReplayMaskNativeComponent.ts b/packages/core/src/js/RNSentryReplayMaskNativeComponent.ts index 794dcb2e8c..db3b868f11 100644 --- a/packages/core/src/js/RNSentryReplayMaskNativeComponent.ts +++ b/packages/core/src/js/RNSentryReplayMaskNativeComponent.ts @@ -1,7 +1,7 @@ import type { HostComponent, ViewProps } from 'react-native'; // The default export exists in the file but eslint doesn't see it // eslint-disable-next-line import/default -import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import { codegenNativeComponent } from 'react-native'; // If changed to type NativeProps = ViewProps, react native codegen will fail finding the NativeProps type // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/packages/core/src/js/RNSentryReplayUnmaskNativeComponent.ts b/packages/core/src/js/RNSentryReplayUnmaskNativeComponent.ts index 928499c747..46ba097095 100644 --- a/packages/core/src/js/RNSentryReplayUnmaskNativeComponent.ts +++ b/packages/core/src/js/RNSentryReplayUnmaskNativeComponent.ts @@ -1,7 +1,7 @@ import type { HostComponent, ViewProps } from 'react-native'; // The default export exists in the file but eslint doesn't see it // eslint-disable-next-line import/default -import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import { codegenNativeComponent } from 'react-native'; // If changed to type NativeProps = ViewProps, react native codegen will fail finding the NativeProps type // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/packages/core/test/feedback/FeedbackWidgetManager.test.tsx b/packages/core/test/feedback/FeedbackWidgetManager.test.tsx index 0f2703e54c..32020bd771 100644 --- a/packages/core/test/feedback/FeedbackWidgetManager.test.tsx +++ b/packages/core/test/feedback/FeedbackWidgetManager.test.tsx @@ -1,5 +1,5 @@ import { debug, getClient, setCurrentClient } from '@sentry/core'; -import { render } from '@testing-library/react-native'; +import { act, render, waitFor } from '@testing-library/react-native'; import * as React from 'react'; import { Appearance, Text } from 'react-native'; import { defaultConfiguration } from '../../src/js/feedback/defaults'; @@ -33,7 +33,7 @@ describe('FeedbackWidgetManager', () => { resetFeedbackWidgetManager(); }); - it('showFeedbackWidget displays the form when FeedbackWidgetProvider is used', () => { + it('showFeedbackWidget displays the form when FeedbackWidgetProvider is used', async () => { mockedIsModalSupported.mockReturnValue(true); const { getByText, getByTestId } = render( @@ -43,7 +43,9 @@ describe('FeedbackWidgetManager', () => { showFeedbackWidget(); - expect(getByTestId('feedback-form-modal')).toBeTruthy(); + await waitFor(() => { + expect(getByTestId('feedback-form-modal')).toBeTruthy(); + }); expect(getByText('App Components')).toBeTruthy(); }); @@ -70,7 +72,7 @@ describe('FeedbackWidgetManager', () => { }).not.toThrow(); }); - it('showFeedbackWidget displays the form with the feedbackIntegration options', () => { + it('showFeedbackWidget displays the form with the feedbackIntegration options', async () => { mockedIsModalSupported.mockReturnValue(true); const { getByPlaceholderText, getByText } = render( @@ -86,11 +88,13 @@ describe('FeedbackWidgetManager', () => { showFeedbackWidget(); - expect(getByPlaceholderText('Custom Message Placeholder')).toBeTruthy(); + await waitFor(() => { + expect(getByPlaceholderText('Custom Message Placeholder')).toBeTruthy(); + }); expect(getByText('Custom Submit Button')).toBeTruthy(); }); - it('showFeedbackWidget displays the form with the feedbackIntegration options merged with the defaults', () => { + it('showFeedbackWidget displays the form with the feedbackIntegration options merged with the defaults', async () => { mockedIsModalSupported.mockReturnValue(true); const { getByPlaceholderText, getByText, queryByText } = render( @@ -105,8 +109,10 @@ describe('FeedbackWidgetManager', () => { showFeedbackWidget(); - expect(queryByText(defaultConfiguration.submitButtonLabel)).toBeFalsy(); // overridden value - expect(getByText('Custom Submit Button')).toBeTruthy(); // overridden value + await waitFor(() => { + expect(queryByText(defaultConfiguration.submitButtonLabel)).toBeFalsy(); // overridden value + expect(getByText('Custom Submit Button')).toBeTruthy(); // overridden value + }); expect(getByPlaceholderText(defaultConfiguration.messagePlaceholder)).toBeTruthy(); // default configuration value }); @@ -161,7 +167,7 @@ describe('FeedbackButtonManager', () => { }); }); - it('showFeedbackButton displays the button when FeedbackWidgetProvider is used', () => { + it('showFeedbackButton displays the button when FeedbackWidgetProvider is used', async () => { const { getByText } = render( App Components @@ -170,7 +176,9 @@ describe('FeedbackButtonManager', () => { showFeedbackButton(); - expect(getByText('Report a Bug')).toBeTruthy(); + await waitFor(() => { + expect(getByText('Report a Bug')).toBeTruthy(); + }) }); it('hideFeedbackButton hides the button', () => { @@ -216,7 +224,7 @@ describe('FeedbackButtonManager', () => { expect(getClient().getIntegrationByName(AUTO_INJECT_FEEDBACK_BUTTON_INTEGRATION_NAME)).toBeDefined(); }); - it('the Feedback Widget matches the snapshot with default configuration and system light theme', () => { + it('the Feedback Widget matches the snapshot with default configuration and system light theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -226,12 +234,14 @@ describe('FeedbackButtonManager', () => { jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('light'); - showFeedbackWidget(); + await act(async () => { + showFeedbackWidget(); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Widget matches the snapshot with default configuration and system dark theme', () => { + it('the Feedback Widget matches the snapshot with default configuration and system dark theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -241,12 +251,14 @@ describe('FeedbackButtonManager', () => { jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('dark'); - showFeedbackWidget(); + await act(async () => { + showFeedbackWidget(); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Widget matches the snapshot with default configuration and dynamically changed theme', () => { + it('the Feedback Widget matches the snapshot with default configuration and dynamically changed theme', async () => { const component = ( App Components @@ -258,15 +270,19 @@ describe('FeedbackButtonManager', () => { jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('light'); - showFeedbackWidget(); + await act(async () => { + showFeedbackWidget(); + }); jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('dark'); - listener({ colorScheme: 'dark' }); + await act(async () => { + listener({ colorScheme: 'dark' }); + }) expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Widget matches the snapshot with custom light theme', () => { + it('the Feedback Widget matches the snapshot with custom light theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -283,12 +299,14 @@ describe('FeedbackButtonManager', () => { }); getClient()?.addIntegration(integration); - showFeedbackWidget(); + await act(async () => { + showFeedbackWidget(); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Widget matches the snapshot with custom dark theme', () => { + it('the Feedback Widget matches the snapshot with custom dark theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -305,12 +323,14 @@ describe('FeedbackButtonManager', () => { }); getClient()?.addIntegration(integration); - showFeedbackWidget(); + await act(async () => { + showFeedbackWidget(); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Widget matches the snapshot with system light custom theme', () => { + it('the Feedback Widget matches the snapshot with system light custom theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -329,12 +349,14 @@ describe('FeedbackButtonManager', () => { jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('light'); - showFeedbackWidget(); + await act(async () => { + showFeedbackWidget(); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Widget matches the snapshot with system dark custom theme', () => { + it('the Feedback Widget matches the snapshot with system dark custom theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -353,12 +375,14 @@ describe('FeedbackButtonManager', () => { jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('dark'); - showFeedbackWidget(); + await act(async () => { + showFeedbackWidget(); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Button matches the snapshot with default configuration and system light theme', () => { + it('the Feedback Button matches the snapshot with default configuration and system light theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -368,12 +392,14 @@ describe('FeedbackButtonManager', () => { jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('light'); - showFeedbackButton(); + await act(async () => { + showFeedbackButton(); + }) expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Button matches the snapshot with default configuration and system dark theme', () => { + it('the Feedback Button matches the snapshot with default configuration and system dark theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -383,12 +409,14 @@ describe('FeedbackButtonManager', () => { jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('dark'); - showFeedbackButton(); + await act(async () => { + showFeedbackButton(); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Button matches the snapshot with default configuration and dynamically changed theme', () => { + it('the Feedback Button matches the snapshot with default configuration and dynamically changed theme', async () => { const component = ( App Components @@ -400,15 +428,19 @@ describe('FeedbackButtonManager', () => { jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('light'); - showFeedbackButton(); + await act(async () => { + showFeedbackButton(); + }); jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('dark'); - listener({ colorScheme: 'dark' }); + await act(async () => { + listener({ colorScheme: 'dark' }); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Button matches the snapshot with custom light theme', () => { + it('the Feedback Button matches the snapshot with custom light theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -425,12 +457,14 @@ describe('FeedbackButtonManager', () => { }); getClient()?.addIntegration(integration); - showFeedbackButton(); + await act(async () => { + showFeedbackButton(); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Button matches the snapshot with custom dark theme', () => { + it('the Feedback Button matches the snapshot with custom dark theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -447,12 +481,14 @@ describe('FeedbackButtonManager', () => { }); getClient()?.addIntegration(integration); - showFeedbackButton(); + await act(async () => { + showFeedbackButton(); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Button matches the snapshot with system light custom theme', () => { + it('the Feedback Button matches the snapshot with system light custom theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -471,12 +507,14 @@ describe('FeedbackButtonManager', () => { jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('light'); - showFeedbackButton(); + await act(async () => { + showFeedbackButton(); + }); expect(toJSON()).toMatchSnapshot(); }); - it('the Feedback Button matches the snapshot with system dark custom theme', () => { + it('the Feedback Button matches the snapshot with system dark custom theme', async () => { mockedIsModalSupported.mockReturnValue(true); const { toJSON } = render( @@ -495,7 +533,9 @@ describe('FeedbackButtonManager', () => { jest.spyOn(Appearance, 'getColorScheme').mockReturnValue('dark'); - showFeedbackButton(); + await act(async () => { + showFeedbackButton(); + }); expect(toJSON()).toMatchSnapshot(); }); diff --git a/packages/core/test/feedback/ScreenshotButton.test.tsx b/packages/core/test/feedback/ScreenshotButton.test.tsx index 0bacceb208..e23fc1323c 100644 --- a/packages/core/test/feedback/ScreenshotButton.test.tsx +++ b/packages/core/test/feedback/ScreenshotButton.test.tsx @@ -83,10 +83,9 @@ describe('ScreenshotButton', () => { enableTakeScreenshot: true, }); getClient()?.addIntegration(integration); - showFeedbackButton(); - fireEvent.press(getByText('Report a Bug')); + await waitFor(() => fireEvent.press(getByText('Report a Bug'))); const takeScreenshotButton = getByText('Take a screenshot'); expect(takeScreenshotButton).toBeTruthy(); @@ -107,8 +106,10 @@ describe('ScreenshotButton', () => { showFeedbackButton(); - fireEvent.press(getByText('Report a Bug')); - fireEvent.press(getByText('Take a screenshot')); + await waitFor(() => { + fireEvent.press(getByText('Report a Bug')); + fireEvent.press(getByText('Take a screenshot')); + }); const captureButton = getByText('Take Screenshot'); expect(captureButton).toBeTruthy(); @@ -131,9 +132,11 @@ describe('ScreenshotButton', () => { showFeedbackButton(); - fireEvent.press(getByText('Report a Bug')); - fireEvent.press(getByText('Take a screenshot')); - fireEvent.press(getByText('Take Screenshot')); + await waitFor(() => { + fireEvent.press(getByText('Report a Bug')); + fireEvent.press(getByText('Take a screenshot')); + fireEvent.press(getByText('Take Screenshot')); + }); await waitFor(() => { expect(mockCaptureScreenshot).toHaveBeenCalled(); @@ -161,9 +164,11 @@ describe('ScreenshotButton', () => { showFeedbackButton(); - fireEvent.press(getByText('Report a Bug')); - fireEvent.press(getByText('Take a screenshot')); - fireEvent.press(getByText('Take Screenshot')); + await waitFor(() => { + fireEvent.press(getByText('Report a Bug')); + fireEvent.press(getByText('Take a screenshot')); + fireEvent.press(getByText('Take Screenshot')); + }); await waitFor(() => { expect(mockCaptureScreenshot).toHaveBeenCalled(); @@ -205,9 +210,11 @@ describe('ScreenshotButton', () => { showFeedbackButton(); - fireEvent.press(getByText('Report a Bug')); - fireEvent.press(getByText('Take a screenshot')); - fireEvent.press(getByText('Take Screenshot')); + await waitFor(() => { + fireEvent.press(getByText('Report a Bug')); + fireEvent.press(getByText('Take a screenshot')); + fireEvent.press(getByText('Take Screenshot')); + }); await waitFor(() => { expect(mockCaptureScreenshot).toHaveBeenCalled(); diff --git a/packages/core/test/feedback/__snapshots__/FeedbackWidgetManager.test.tsx.snap b/packages/core/test/feedback/__snapshots__/FeedbackWidgetManager.test.tsx.snap index 521573f3ad..0c8e53bdee 100644 --- a/packages/core/test/feedback/__snapshots__/FeedbackWidgetManager.test.tsx.snap +++ b/packages/core/test/feedback/__snapshots__/FeedbackWidgetManager.test.tsx.snap @@ -650,7 +650,6 @@ exports[`FeedbackButtonManager the Feedback Widget matches the snapshot with cus > void; removeSubscription: jest.Func; }; -const mockedAppState: AppState & MockAppState = { - removeSubscription: jest.fn(), - listener: jest.fn(), - isAvailable: true, - currentState: 'active', - addEventListener: (_, listener) => { - mockedAppState.listener = listener; - return { - remove: mockedAppState.removeSubscription, - }; - }, - setState: (state: AppStateStatus) => { - mockedAppState.currentState = state; - mockedAppState.listener(state); - }, -}; -jest.mock('react-native/Libraries/AppState/AppState', () => mockedAppState); +jest.mock('react-native', () => { + const mockedAppState: AppState & MockAppState = { + removeSubscription: jest.fn(), + listener: jest.fn(), + isAvailable: true, + currentState: 'active', + addEventListener: jest.fn(), + setState: (state: AppStateStatus) => { + mockedAppState.currentState = state; + mockedAppState.listener(state); + }, + }; + return { + AppState: mockedAppState, + Platform: { OS: 'ios' }, + NativeModules: { + RNSentry: {}, + }, + }; +}); + +const mockedAppState = AppState as jest.Mocked; describe('startIdleNavigationSpan', () => { beforeEach(() => { @@ -35,12 +41,10 @@ describe('startIdleNavigationSpan', () => { NATIVE.enableNative = true; mockedAppState.isAvailable = true; mockedAppState.currentState = 'active'; - mockedAppState.addEventListener = (_, listener) => { + (mockedAppState.addEventListener as jest.Mock).mockImplementation((_, listener) => { mockedAppState.listener = listener; - return { - remove: mockedAppState.removeSubscription, - }; - }; + return { remove: mockedAppState.removeSubscription }; + }); setupTestClient(); }); @@ -66,9 +70,9 @@ describe('startIdleNavigationSpan', () => { it('Does not crash when AppState is not available', async () => { mockedAppState.isAvailable = false; - mockedAppState.addEventListener = ((): void => { + (mockedAppState.addEventListener as jest.Mock).mockImplementation(() => { return undefined; - }) as unknown as (typeof mockedAppState)['addEventListener']; // RN Web can return undefined + }); startIdleNavigationSpan({ name: 'test', @@ -94,7 +98,7 @@ describe('startIdleNavigationSpan', () => { // Verify it's a non-recording span expect(span).toBeDefined(); - expect(span.constructor.name).toBe('SentryNonRecordingSpan'); + expect(span?.constructor.name).toBe('SentryNonRecordingSpan'); // No AppState listener should be set up for non-recording spans expect(mockedAppState.removeSubscription).not.toHaveBeenCalled(); @@ -140,7 +144,7 @@ describe('startIdleNavigationSpan', () => { name: 'test', }); - firstSpan.end(); + firstSpan?.end(); const secondSpan = startIdleNavigationSpan({ name: 'test', diff --git a/packages/core/test/tracing/integrations/userInteraction.test.ts b/packages/core/test/tracing/integrations/userInteraction.test.ts index 04af7738fe..4b983a5390 100644 --- a/packages/core/test/tracing/integrations/userInteraction.test.ts +++ b/packages/core/test/tracing/integrations/userInteraction.test.ts @@ -7,7 +7,8 @@ import { startInactiveSpan, startSpanManual, } from '@sentry/core'; -import type { AppState, AppStateStatus } from 'react-native'; +import type { AppStateStatus } from 'react-native'; +import { AppState } from 'react-native'; import { startUserInteractionSpan, userInteractionIntegration, @@ -26,23 +27,28 @@ type MockAppState = { listener: (newState: AppStateStatus) => void; removeSubscription: jest.Func; }; -const mockedAppState: AppState & MockAppState = { - removeSubscription: jest.fn(), - listener: jest.fn(), - isAvailable: true, - currentState: 'active', - addEventListener: (_, listener) => { - mockedAppState.listener = listener; - return { - remove: mockedAppState.removeSubscription, - }; - }, - setState: (state: AppStateStatus) => { - mockedAppState.currentState = state; - mockedAppState.listener(state); - }, -}; -jest.mock('react-native/Libraries/AppState/AppState', () => mockedAppState); +jest.mock('react-native', () => { + const mockedAppState: AppState & MockAppState = { + removeSubscription: jest.fn(), + listener: jest.fn(), + isAvailable: true, + currentState: 'active', + addEventListener: jest.fn(), + setState: (state: AppStateStatus) => { + mockedAppState.currentState = state; + mockedAppState.listener(state); + }, + }; + return { + AppState: mockedAppState, + Platform: { OS: 'ios' }, + NativeModules: { + RNSentry: {}, + }, + }; +}); + +const mockedAppState = AppState as jest.Mocked; jest.mock('../../../src/js/wrapper', () => { return { @@ -69,12 +75,10 @@ describe('User Interaction Tracing', () => { NATIVE.enableNative = true; mockedAppState.isAvailable = true; mockedAppState.currentState = 'active'; - mockedAppState.addEventListener = (_, listener) => { + (mockedAppState.addEventListener as jest.Mock).mockImplementation((_, listener) => { mockedAppState.listener = listener; - return { - remove: mockedAppState.removeSubscription, - }; - }; + return { remove: mockedAppState.removeSubscription }; + }); mockedUserInteractionId = { elementId: 'mockedElementId', op: 'mocked.op' }; client = setupTestClient({ diff --git a/packages/core/test/wrap.mocked.test.tsx b/packages/core/test/wrap.mocked.test.tsx index ee9fda34bb..e844f33c1c 100644 --- a/packages/core/test/wrap.mocked.test.tsx +++ b/packages/core/test/wrap.mocked.test.tsx @@ -116,7 +116,7 @@ describe('Sentry.wrap', () => { includeRender: true, includeUpdates: true, }), - expect.anything(), + expect.toBeNil() ); expect(ReactNativeProfiler).not.toHaveBeenCalledWith( @@ -137,7 +137,7 @@ describe('Sentry.wrap', () => { name: 'Root', updateProps: {}, }), - expect.anything(), + expect.toBeNil(), ); }); }); diff --git a/yarn.lock b/yarn.lock index 8234bb5563..a5d402167b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8299,6 +8299,13 @@ __metadata: languageName: node linkType: hard +"@react-native/assets-registry@npm:0.80.1": + version: 0.80.1 + resolution: "@react-native/assets-registry@npm:0.80.1" + checksum: 21954f7030d8589fcbebd9d4652eed3768dc95fa5af1c1a27d3b21f4ed906a3af5432dab4f0d2eecb709acce7b1768acf80fe5e0883a2715113312edcbb12226 + languageName: node + linkType: hard + "@react-native/assets-registry@npm:0.80.2": version: 0.80.2 resolution: "@react-native/assets-registry@npm:0.80.2" @@ -8342,6 +8349,16 @@ __metadata: languageName: node linkType: hard +"@react-native/babel-plugin-codegen@npm:0.80.0": + version: 0.80.0 + resolution: "@react-native/babel-plugin-codegen@npm:0.80.0" + dependencies: + "@babel/traverse": ^7.25.3 + "@react-native/codegen": 0.80.0 + checksum: 0ed0fb4e55af18deec9e276a6b015e3f52f5a44b8baaab84e46ea832509fd7bc84533d772dd660fe856b11fcba06166d8ae5295fa5722b2ecd424ef824844a2d + languageName: node + linkType: hard + "@react-native/babel-plugin-codegen@npm:0.80.2": version: 0.80.2 resolution: "@react-native/babel-plugin-codegen@npm:0.80.2" @@ -8514,6 +8531,61 @@ __metadata: languageName: node linkType: hard +"@react-native/babel-preset@npm:0.80.0": + version: 0.80.0 + resolution: "@react-native/babel-preset@npm:0.80.0" + dependencies: + "@babel/core": ^7.25.2 + "@babel/plugin-proposal-export-default-from": ^7.24.7 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + "@babel/plugin-syntax-export-default-from": ^7.24.7 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-transform-arrow-functions": ^7.24.7 + "@babel/plugin-transform-async-generator-functions": ^7.25.4 + "@babel/plugin-transform-async-to-generator": ^7.24.7 + "@babel/plugin-transform-block-scoping": ^7.25.0 + "@babel/plugin-transform-class-properties": ^7.25.4 + "@babel/plugin-transform-classes": ^7.25.4 + "@babel/plugin-transform-computed-properties": ^7.24.7 + "@babel/plugin-transform-destructuring": ^7.24.8 + "@babel/plugin-transform-flow-strip-types": ^7.25.2 + "@babel/plugin-transform-for-of": ^7.24.7 + "@babel/plugin-transform-function-name": ^7.25.1 + "@babel/plugin-transform-literals": ^7.25.2 + "@babel/plugin-transform-logical-assignment-operators": ^7.24.7 + "@babel/plugin-transform-modules-commonjs": ^7.24.8 + "@babel/plugin-transform-named-capturing-groups-regex": ^7.24.7 + "@babel/plugin-transform-nullish-coalescing-operator": ^7.24.7 + "@babel/plugin-transform-numeric-separator": ^7.24.7 + "@babel/plugin-transform-object-rest-spread": ^7.24.7 + "@babel/plugin-transform-optional-catch-binding": ^7.24.7 + "@babel/plugin-transform-optional-chaining": ^7.24.8 + "@babel/plugin-transform-parameters": ^7.24.7 + "@babel/plugin-transform-private-methods": ^7.24.7 + "@babel/plugin-transform-private-property-in-object": ^7.24.7 + "@babel/plugin-transform-react-display-name": ^7.24.7 + "@babel/plugin-transform-react-jsx": ^7.25.2 + "@babel/plugin-transform-react-jsx-self": ^7.24.7 + "@babel/plugin-transform-react-jsx-source": ^7.24.7 + "@babel/plugin-transform-regenerator": ^7.24.7 + "@babel/plugin-transform-runtime": ^7.24.7 + "@babel/plugin-transform-shorthand-properties": ^7.24.7 + "@babel/plugin-transform-spread": ^7.24.7 + "@babel/plugin-transform-sticky-regex": ^7.24.7 + "@babel/plugin-transform-typescript": ^7.25.2 + "@babel/plugin-transform-unicode-regex": ^7.24.7 + "@babel/template": ^7.25.0 + "@react-native/babel-plugin-codegen": 0.80.0 + babel-plugin-syntax-hermes-parser: 0.28.1 + babel-plugin-transform-flow-enums: ^0.0.2 + react-refresh: ^0.14.0 + peerDependencies: + "@babel/core": "*" + checksum: b63684f53651e236bcc19888c51407b9e0022012e7171a0e7dde2d93662fede960ea420c18896d656e688cff0590ac5d872bc8d3523ed3baacd71c865a2b9992 + languageName: node + linkType: hard + "@react-native/babel-preset@npm:0.80.2": version: 0.80.2 resolution: "@react-native/babel-preset@npm:0.80.2" @@ -8618,6 +8690,36 @@ __metadata: languageName: node linkType: hard +"@react-native/codegen@npm:0.80.0": + version: 0.80.0 + resolution: "@react-native/codegen@npm:0.80.0" + dependencies: + glob: ^7.1.1 + hermes-parser: 0.28.1 + invariant: ^2.2.4 + nullthrows: ^1.1.1 + yargs: ^17.6.2 + peerDependencies: + "@babel/core": "*" + checksum: 5ce4842964e1eecebbdcad44c0b152df9f9c002753fb21a3cb4a8d68967896813b4a960a38a4508af42a7b61737e4cbe1a8a819f98a08705c56ca6d1f221e87b + languageName: node + linkType: hard + +"@react-native/codegen@npm:0.80.1": + version: 0.80.1 + resolution: "@react-native/codegen@npm:0.80.1" + dependencies: + glob: ^7.1.1 + hermes-parser: 0.28.1 + invariant: ^2.2.4 + nullthrows: ^1.1.1 + yargs: ^17.6.2 + peerDependencies: + "@babel/core": "*" + checksum: 18149038e9bfa185f8f258c1482cba954a101d425f0e5aa8e14f6e31d811569af871aeba1e369cecbbdc13d88c44383b584fefef8d8b896c955b3f2ec6aa6755 + languageName: node + linkType: hard + "@react-native/codegen@npm:0.80.2": version: 0.80.2 resolution: "@react-native/codegen@npm:0.80.2" @@ -8715,6 +8817,27 @@ __metadata: languageName: node linkType: hard +"@react-native/community-cli-plugin@npm:0.80.1": + version: 0.80.1 + resolution: "@react-native/community-cli-plugin@npm:0.80.1" + dependencies: + "@react-native/dev-middleware": 0.80.1 + chalk: ^4.0.0 + debug: ^4.4.0 + invariant: ^2.2.4 + metro: ^0.82.2 + metro-config: ^0.82.2 + metro-core: ^0.82.2 + semver: ^7.1.3 + peerDependencies: + "@react-native-community/cli": "*" + peerDependenciesMeta: + "@react-native-community/cli": + optional: true + checksum: b25348dd48699765da4c8c5a0bfb02c7bb1c8e5623a963f47b553f9b0e6189e770fd1e5620b6e046eb3191c621ea2fb1d8ea57c2f3e98ec37a89e7d7938cf68d + languageName: node + linkType: hard + "@react-native/community-cli-plugin@npm:0.80.2": version: 0.80.2 resolution: "@react-native/community-cli-plugin@npm:0.80.2" @@ -8757,6 +8880,13 @@ __metadata: languageName: node linkType: hard +"@react-native/debugger-frontend@npm:0.80.1": + version: 0.80.1 + resolution: "@react-native/debugger-frontend@npm:0.80.1" + checksum: e657acfa2023f873f834a9dcdd320f2cda952f9f208e7c367a87a049ed2ed576ea00ba58f346514560fc8c530a52e4a88fc43f7577e70619e2d7f9da318cc897 + languageName: node + linkType: hard + "@react-native/debugger-frontend@npm:0.80.2": version: 0.80.2 resolution: "@react-native/debugger-frontend@npm:0.80.2" @@ -8822,6 +8952,25 @@ __metadata: languageName: node linkType: hard +"@react-native/dev-middleware@npm:0.80.1": + version: 0.80.1 + resolution: "@react-native/dev-middleware@npm:0.80.1" + dependencies: + "@isaacs/ttlcache": ^1.4.1 + "@react-native/debugger-frontend": 0.80.1 + chrome-launcher: ^0.15.2 + chromium-edge-launcher: ^0.2.0 + connect: ^3.6.5 + debug: ^4.4.0 + invariant: ^2.2.4 + nullthrows: ^1.1.1 + open: ^7.0.3 + serve-static: ^1.16.2 + ws: ^6.2.3 + checksum: 6f501af16558a3d9cbef9de2ede4148b57a4c163bfc5f31641ea3eb8343ae965e3a5713e1037d5a3dae239bf47faa7998dcd3d1d9a7dd6253889d1eda901d8a0 + languageName: node + linkType: hard + "@react-native/dev-middleware@npm:0.80.2": version: 0.80.2 resolution: "@react-native/dev-middleware@npm:0.80.2" @@ -8923,6 +9072,13 @@ __metadata: languageName: node linkType: hard +"@react-native/gradle-plugin@npm:0.80.1": + version: 0.80.1 + resolution: "@react-native/gradle-plugin@npm:0.80.1" + checksum: f993cd66ef383239e55dd7f38dba22b4a727cf67652c202862fa8736974a3c47a70116ff2f1e591508e0011be03bf6bb30ffc52a6f3bcfbbe9c7962c4b60eefa + languageName: node + linkType: hard + "@react-native/gradle-plugin@npm:0.80.2": version: 0.80.2 resolution: "@react-native/gradle-plugin@npm:0.80.2" @@ -8951,6 +9107,13 @@ __metadata: languageName: node linkType: hard +"@react-native/js-polyfills@npm:0.80.1": + version: 0.80.1 + resolution: "@react-native/js-polyfills@npm:0.80.1" + checksum: 1f765fb724d0940ccc291de42be7689d062286eac1c0d8237a99fe4a5ee6ce84e0250c6c779829534120d85bddb427907aa225250853d0086c8cbc1385df6b83 + languageName: node + linkType: hard + "@react-native/js-polyfills@npm:0.80.2": version: 0.80.2 resolution: "@react-native/js-polyfills@npm:0.80.2" @@ -9073,6 +9236,13 @@ __metadata: languageName: node linkType: hard +"@react-native/normalize-colors@npm:0.80.1": + version: 0.80.1 + resolution: "@react-native/normalize-colors@npm:0.80.1" + checksum: cc3f09165bbfb921a521d72e38a2d827d31cf41b8a9bb32b4c4f9bbadcbf78fdfebd9fa1aa23bc0c192f6383660c1969ad63b3ebff9481893179468da2bfe7d2 + languageName: node + linkType: hard + "@react-native/normalize-colors@npm:0.80.2": version: 0.80.2 resolution: "@react-native/normalize-colors@npm:0.80.2" @@ -9154,6 +9324,23 @@ __metadata: languageName: node linkType: hard +"@react-native/virtualized-lists@npm:0.80.1": + version: 0.80.1 + resolution: "@react-native/virtualized-lists@npm:0.80.1" + dependencies: + invariant: ^2.2.4 + nullthrows: ^1.1.1 + peerDependencies: + "@types/react": ^19.0.0 + react: "*" + react-native: "*" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 1e809e2b41f799084a4b8b6304d2dfe8986218e6122c1065413442ff71c0d5e1d267b4c1e241385aa2d96cb6a8a11e5add9e48346f38e42848d82488d6c6e9bb + languageName: node + linkType: hard + "@react-native/virtualized-lists@npm:0.80.2": version: 0.80.2 resolution: "@react-native/virtualized-lists@npm:0.80.2" @@ -9736,10 +9923,10 @@ __metadata: version: 0.0.0-use.local resolution: "@sentry/react-native@workspace:packages/core" dependencies: - "@babel/core": ^7.25.2 + "@babel/core": ^7.26.7 "@expo/metro-config": ~0.20.0 "@mswjs/interceptors": ^0.25.15 - "@react-native/babel-preset": 0.77.1 + "@react-native/babel-preset": 0.80.0 "@sentry-internal/eslint-config-sdk": 10.17.0 "@sentry-internal/eslint-plugin-sdk": 10.17.0 "@sentry-internal/typescript": 10.17.0 @@ -9750,10 +9937,11 @@ __metadata: "@sentry/react": 10.17.0 "@sentry/types": 10.17.0 "@sentry/wizard": 6.5.0 - "@testing-library/react-native": ^12.7.2 + "@testing-library/react-native": ^13.2.2 "@types/jest": ^29.5.13 "@types/node": ^20.9.3 - "@types/react": ^18.2.64 + "@types/react": ^19.1.0 + "@types/react-test-renderer": ^19.1.0 "@types/uglify-js": ^3.17.2 "@types/uuid": ^9.0.4 "@types/xmlhttprequest": ^1.8.2 @@ -9768,14 +9956,15 @@ __metadata: eslint-plugin-react-native: ^3.8.1 expo: ^53.0.0 expo-module-scripts: 3.1.0 - jest: ^29.6.2 + jest: ^29.6.3 jest-environment-jsdom: ^29.6.2 jest-extended: ^4.0.2 madge: ^6.1.0 metro: 0.83.1 prettier: ^2.0.5 - react: 18.3.1 - react-native: 0.77.1 + react: 19.1.0 + react-native: 0.80.1 + react-test-renderer: 19.1.0 rimraf: ^4.1.1 ts-jest: ^29.3.1 typescript: 4.9.5 @@ -10060,25 +10249,6 @@ __metadata: languageName: node linkType: hard -"@testing-library/react-native@npm:^12.7.2": - version: 12.7.2 - resolution: "@testing-library/react-native@npm:12.7.2" - dependencies: - jest-matcher-utils: ^29.7.0 - pretty-format: ^29.7.0 - redent: ^3.0.0 - peerDependencies: - jest: ">=28.0.0" - react: ">=16.8.0" - react-native: ">=0.59" - react-test-renderer: ">=16.8.0" - peerDependenciesMeta: - jest: - optional: true - checksum: 7e3d8ab7d549823fcf438c17353e6c40386da88bbb1edfbd0747282a28c673597be27fdc2fa1f3a7d8786b77c72bb2e37f67ad2c9134225e9b68db97838f77e2 - languageName: node - linkType: hard - "@testing-library/react-native@npm:^13.2.2": version: 13.2.2 resolution: "@testing-library/react-native@npm:13.2.2" @@ -20549,7 +20719,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:^29.6.2, jest@npm:^29.6.3, jest@npm:^29.7.0": +"jest@npm:^29.6.3, jest@npm:^29.7.0": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: @@ -26753,6 +26923,57 @@ __metadata: languageName: node linkType: hard +"react-native@npm:0.80.1": + version: 0.80.1 + resolution: "react-native@npm:0.80.1" + dependencies: + "@jest/create-cache-key-function": ^29.7.0 + "@react-native/assets-registry": 0.80.1 + "@react-native/codegen": 0.80.1 + "@react-native/community-cli-plugin": 0.80.1 + "@react-native/gradle-plugin": 0.80.1 + "@react-native/js-polyfills": 0.80.1 + "@react-native/normalize-colors": 0.80.1 + "@react-native/virtualized-lists": 0.80.1 + abort-controller: ^3.0.0 + anser: ^1.4.9 + ansi-regex: ^5.0.0 + babel-jest: ^29.7.0 + babel-plugin-syntax-hermes-parser: 0.28.1 + base64-js: ^1.5.1 + chalk: ^4.0.0 + commander: ^12.0.0 + flow-enums-runtime: ^0.0.6 + glob: ^7.1.1 + invariant: ^2.2.4 + jest-environment-node: ^29.7.0 + memoize-one: ^5.0.0 + metro-runtime: ^0.82.2 + metro-source-map: ^0.82.2 + nullthrows: ^1.1.1 + pretty-format: ^29.7.0 + promise: ^8.3.0 + react-devtools-core: ^6.1.1 + react-refresh: ^0.14.0 + regenerator-runtime: ^0.13.2 + scheduler: 0.26.0 + semver: ^7.1.3 + stacktrace-parser: ^0.1.10 + whatwg-fetch: ^3.0.0 + ws: ^6.2.3 + yargs: ^17.6.2 + peerDependencies: + "@types/react": ^19.1.0 + react: ^19.1.0 + peerDependenciesMeta: + "@types/react": + optional: true + bin: + react-native: cli.js + checksum: 4eb0675ef268d16d686411420a1744f62a6eec67972bc575b5e85049bec11d90be3103ee36fd83e51c297741afb6b7aff2d133ebe8765c855c1351befab2cd18 + languageName: node + linkType: hard + "react-native@npm:0.80.2": version: 0.80.2 resolution: "react-native@npm:0.80.2"