Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PLAT-11855] Forward navigation refs #431

Merged
merged 8 commits into from Mar 27, 2024
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -16,6 +16,10 @@
- Rename package import from `@bugsnag/react-native-navigation-performance` to `@bugsnag/plugin-react-native-navigation-performance`
- Rename plugin type from `ReactNativeNavigationPlugin` to `BugsnagPluginReactNativeNavigationPerformance`

### Fixed

- (plugin-react-navigation) Fix an issue where refs are not forwarded to the NavigationContainer [#431](https://github.com/bugsnag/bugsnag-js-performance/pull/431)

## v2.3.0 (2024-03-20)

This release adds support for instrumenting navigation spans when using the [react-native-navigation](https://github.com/wix/react-native-navigation) library
Expand Down
7 changes: 4 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -1,44 +1,44 @@
import { type SpanFactory } from '@bugsnag/core-performance'
import { type ReactNativeConfiguration } from '@bugsnag/react-native-performance'
import { NavigationContainer, useNavigationContainerRef, type NavigationContainerProps } from '@react-navigation/native'
import { NavigationContainer, useNavigationContainerRef, type NavigationContainerProps, type NavigationContainerRefWithCurrent } from '@react-navigation/native'
import React, { forwardRef, useRef } from 'react'
import { NavigationContextProvider } from './navigation-context'

// Prevent rollup plugin from tree shaking NavigationContextProvider
const Provider = NavigationContextProvider

export const createNavigationContainer = (Container = NavigationContainer, spanFactory: SpanFactory<ReactNativeConfiguration>) =>
forwardRef<typeof Container, NavigationContainerProps>(
(props, _ref) => {
const { onStateChange, ...rest } = props
type CreateNavigationContainer = (NavigationContainerComponent: typeof NavigationContainer, spanFactory: SpanFactory<ReactNativeConfiguration>) => typeof NavigationContainer
type NavigationContainerRef = NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>

const navigationContainerRef = useNavigationContainerRef()
const [routeName, setRouteName] = React.useState<string>()
const routeNameRef = useRef<string>()
export const createNavigationContainer: CreateNavigationContainer = (NavigationContainerComponent = NavigationContainer, spanFactory: SpanFactory<ReactNativeConfiguration>) => {
return forwardRef<NavigationContainerRef, NavigationContainerProps>((props, _ref) => {
const { onStateChange, ...rest } = props

const wrappedOnStateChange: typeof onStateChange = (...args) => {
const currentRoute = navigationContainerRef.current
? navigationContainerRef.current.getCurrentRoute()
: null
const navigationContainerRef = _ref as NavigationContainerRef || useNavigationContainerRef()
const [routeName, setRouteName] = React.useState<string>()
const routeNameRef = useRef<string>()

if (currentRoute) {
routeNameRef.current = currentRoute.name
setRouteName(currentRoute.name)
}
const wrappedOnStateChange: typeof onStateChange = (...args) => {
const currentRoute = navigationContainerRef ? navigationContainerRef.getCurrentRoute() : null

if (typeof onStateChange === 'function') {
onStateChange.apply(this, args)
}
if (currentRoute) {
routeNameRef.current = currentRoute.name
setRouteName(currentRoute.name)
}

return (
<Provider spanFactory={spanFactory} currentRoute={routeName}>
<Container
{...rest}
onStateChange={wrappedOnStateChange}
ref={navigationContainerRef}
/>
</Provider>
)
if (typeof onStateChange === 'function') {
onStateChange.apply(this, args)
}
}
)

return (
<Provider spanFactory={spanFactory} currentRoute={routeName}>
<NavigationContainerComponent
{...rest}
onStateChange={wrappedOnStateChange}
ref={navigationContainerRef}
/>
</Provider>
)
}) as typeof NavigationContainerComponent
}
@@ -1,9 +1,10 @@
import { MockSpanFactory } from '@bugsnag/js-performance-test-utilities'
import { type ReactNativeConfiguration } from '@bugsnag/react-native-performance'
import { NavigationContainer, type ParamListBase } from '@react-navigation/native'
import { NavigationContainer, createNavigationContainerRef, type ParamListBase } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import '@testing-library/jest-native/extend-expect'
import { fireEvent, render, screen } from '@testing-library/react-native'
import { act, fireEvent, render, screen } from '@testing-library/react-native'
import React from 'react'
import { Button, Text, View } from 'react-native'
import { createNavigationContainer } from '../lib/create-navigation-container'

Expand All @@ -17,20 +18,39 @@ describe('createNavigationContainer', () => {
<RootNavigator />
</BugsnagNavigationContainer>
)
fireEvent.press(screen.getByText('Go to route 2'))

fireEvent.press(screen.getByText('Go to route 2'))
expect(screen.getByText('Route 2')).toBeOnTheScreen()

expect(spanFactory.startSpan).toHaveBeenCalledTimes(1)
expect(spanFactory.startSpan).toHaveBeenCalledWith('[Navigation]Route 2', { isFirstClass: true })

fireEvent.press(screen.getByText('Go back'))

expect(screen.getByText('Route 1')).toBeOnTheScreen()

expect(spanFactory.startSpan).toHaveBeenCalledTimes(2)
expect(spanFactory.startSpan).toHaveBeenCalledWith('[Navigation]Route 1', { isFirstClass: true })
})

it('forwards the provided ref to the NavigationContainer', () => {
const navigationRef = createNavigationContainerRef()
const spanFactory = new MockSpanFactory<ReactNativeConfiguration>()
const BugsnagNavigationContainer = createNavigationContainer(NavigationContainer, spanFactory)

render(
<BugsnagNavigationContainer ref={navigationRef}>
<RootNavigator />
</BugsnagNavigationContainer>
)

act(() => {
// @ts-expect-error navigate method expects type 'never'
navigationRef.navigate('Route 2')
})

expect(spanFactory.startSpan).toHaveBeenCalledTimes(1)
expect(spanFactory.startSpan).toHaveBeenCalledWith('[Navigation]Route 2', { isFirstClass: true })
})
})

interface RootStackParamList extends ParamListBase {
Expand Down