Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/core/__tests__/rn/createApp.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ jest.mock('@mpxjs/utils', () => Object.assign({}, jest.requireActual('@mpxjs/uti

jest.mock('@mpxjs/api-proxy', () => ({ createSelectorQuery: jest.fn(), createIntersectionObserver: jest.fn() }))

jest.mock('@react-navigation/native', () => ({
useNavigationState: selector => selector({ routes: [{ key: 'home' }] }),
useRoute: () => ({ key: 'home' })
}), { virtual: true })

jest.mock('../../src/index', () => ({ config: { rnConfig: {}, ignoreProxyWhiteList: [] }, prototype: {} }))

jest.mock('react-native', () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable space-before-function-paren */
import { useState, useMemo, memo } from 'react'
import { useNavigationState, useRoute } from '@react-navigation/native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { StatusBar, processColor, TouchableWithoutFeedback, Image, View, StyleSheet, Text } from 'react-native'

Expand Down Expand Up @@ -89,6 +90,8 @@ const BACK_ICON =
const MpxNav = memo(({ pageConfig, navigation }: MpxNavProps) => {
const [innerPageConfig, setPageConfig] = useState<PageConfig>(pageConfig || {})
const safeAreaTop = useSafeAreaInsets()?.top || 0
const route = useRoute()
const isFirstPage = useNavigationState(state => state.routes[0].key === route.key)

navigation.setPageConfig = (config: PageConfig) => {
setPageConfig((previousConfig) => Object.assign({}, previousConfig, config))
Expand All @@ -104,18 +107,16 @@ const MpxNav = memo(({ pageConfig, navigation }: MpxNavProps) => {
)

if (isCustom) return statusBarElement
// 假设是栈导航,获取栈的长度
const stackLength = navigation.getState()?.routes?.length
const onStackTopBack = mpxGlobal?.__mpx?.config?.rnConfig?.onStackTopBack
const isHandleStackTopBack = typeof onStackTopBack === 'function'

// 回退按钮与图标
// prettier-ignore
const backElement = stackLength > 1 || isHandleStackTopBack
const backElement = !isFirstPage || isHandleStackTopBack
? (
<TouchableWithoutFeedback
onPress={() => {
if (stackLength <= 1 && isHandleStackTopBack) {
if (isFirstPage && isHandleStackTopBack) {
onStackTopBack()
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ declare let global: {
declare let mpxGlobal: Record<string, any>

declare module '@react-navigation/native' {
export function useNavigationState<T>(selector: (state: { routes: { key: string }[] }) => T): T
export function useRoute (): { key: string }
export function useNavigation (): Record<string, any>
export function usePreventRemove(
enabled: boolean,
Expand Down
104 changes: 100 additions & 4 deletions packages/webpack-plugin/test/runtime/react-native/mpx-nav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,57 @@ jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 20 })
}))

jest.mock('@react-navigation/native', () => {
const React = jest.requireActual('react')
const NavigationContext = React.createContext(null)
return {
NavigationContext,
useRoute: () => React.useContext(NavigationContext).route,
useNavigationState: (selector: (state: any) => boolean) => {
const { navigation } = React.useContext(NavigationContext)
return React.useSyncExternalStore(
React.useCallback((callback: () => void) => navigation.addListener('state', callback), [navigation]),
() => selector(navigation.getState())
)
}
}
}, { virtual: true })

const { NavigationContext } = jest.requireMock('@react-navigation/native')
const renderNav = (navigation: any, pageConfig = {}, routeKey = 'home') => React.createElement(
NavigationContext.Provider, { value: { navigation, route: { key: routeKey } } },
React.createElement(MpxNav, { pageConfig, navigation })
)

describe('RN built-in navigation bar', () => {
const createNavigation = (routeKeys = ['home']) => {
let state = { routes: routeKeys.map(key => ({ key })) }
const listeners = new Set<() => void>()
return {
getState: () => state,
goBack: jest.fn(),
addListener: jest.fn((event: string, callback: () => void) => {
expect(event).toBe('state')
listeners.add(callback)
return () => listeners.delete(callback)
}),
setRoutes: (keys: string[], notify = true) => {
state = { routes: keys.map(key => ({ key })) }
if (notify) listeners.forEach(callback => callback())
},
listeners
}
}

afterEach(() => {
delete (global as any).__mpx.config.rnConfig
})

test('merges consecutive title and color updates in the same batch', () => {
const navigation: any = { getState: () => ({ routes: [{}] }) }
const navigation: any = createNavigation()
let renderer: any
act(() => {
renderer = create(React.createElement(MpxNav, {
pageConfig: { navigationBarTitleText: 'Initial' }, navigation
}))
renderer = create(renderNav(navigation, { navigationBarTitleText: 'Initial' }))
})
act(() => {
navigation.setPageConfig({ navigationBarTitleText: 'Updated' })
Expand All @@ -36,4 +79,57 @@ describe('RN built-in navigation bar', () => {
expect(renderer.root.findByType('StatusBar').props.barStyle).toBe('dark-content')
act(() => renderer.unmount())
})

test('keeps the home page identity while a detail page is open and after returning', () => {
const onStackTopBack = jest.fn()
;(global as any).__mpx.config.rnConfig = { onStackTopBack }
const navigation = createNavigation(['home', 'detail'])
let renderer: any
act(() => {
renderer = create(renderNav(navigation))
})
renderer.root.findByType('TouchableWithoutFeedback').props.onPress()
expect(onStackTopBack).toHaveBeenCalledTimes(1)
expect(navigation.goBack).not.toHaveBeenCalled()

act(() => renderer.update(renderNav(navigation, {}, 'detail')))
renderer.root.findByType('TouchableWithoutFeedback').props.onPress()
expect(navigation.goBack).toHaveBeenCalledTimes(1)

// 模拟后台首页重渲染后,栈已回退但导航栏尚未重渲染。
act(() => renderer.update(renderNav(navigation)))
const onPress = renderer.root.findByType('TouchableWithoutFeedback').props.onPress
navigation.setRoutes(['home'], false)
onPress()
expect(onStackTopBack).toHaveBeenCalledTimes(2)
expect(navigation.goBack).toHaveBeenCalledTimes(1)
act(() => renderer.unmount())
})

test('updates back button visibility on state changes and cleans up subscriptions', () => {
const navigation = createNavigation()
let renderer: any
act(() => {
renderer = create(renderNav(navigation))
})
expect(renderer.root.findAllByType('TouchableWithoutFeedback')).toHaveLength(0)
act(() => navigation.setRoutes(['home', 'detail']))
expect(renderer.root.findAllByType('TouchableWithoutFeedback')).toHaveLength(0)
act(() => navigation.setRoutes(['detail', 'home']))
expect(renderer.root.findAllByType('TouchableWithoutFeedback')).toHaveLength(1)
act(() => navigation.setRoutes(['home']))
expect(renderer.root.findAllByType('TouchableWithoutFeedback')).toHaveLength(0)

const nextNavigation = createNavigation(['detail', 'home'])
act(() => {
renderer.update(renderNav(nextNavigation))
})
expect(navigation.listeners.size).toBe(0)
expect(nextNavigation.listeners.size).toBe(1)
act(() => renderer.root.findByType('TouchableWithoutFeedback').props.onPress())
expect(nextNavigation.goBack).toHaveBeenCalledTimes(1)
expect(navigation.goBack).not.toHaveBeenCalled()
act(() => renderer.unmount())
expect(nextNavigation.listeners.size).toBe(0)
})
})
Loading