one
Version:
One is a new React Framework that makes Vite serve both native and web.
154 lines (139 loc) • 5.38 kB
text/typescript
import { describe, expect, it } from 'vitest'
import type { ParamListBase, StackNavigationState } from '@react-navigation/native'
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack'
import {
convertStackStateToNonOverlayState,
findLastNonOverlayIndex,
isOverlayPresentation,
isTransparentOverlay,
} from '../stackStateUtils'
type Descriptors = Record<string, { options: NativeStackNavigationOptions }>
function makeState(
routeNames: string[],
index: number
): StackNavigationState<ParamListBase> {
const routes = routeNames.map((name) => ({
key: `${name}-key`,
name,
params: undefined,
})) as StackNavigationState<ParamListBase>['routes']
return {
key: 'stack',
index,
routeNames,
routes,
type: 'stack',
stale: false,
preloadedRoutes: [],
} as unknown as StackNavigationState<ParamListBase>
}
function makeDescriptors(
routeNames: string[],
presentations: Record<string, NativeStackNavigationOptions['presentation']>
): Descriptors {
const out: Descriptors = {}
for (const name of routeNames) {
out[`${name}-key`] = { options: { presentation: presentations[name] } }
}
return out
}
describe('isOverlayPresentation', () => {
it('returns true for known overlay presentations', () => {
for (const p of [
'modal',
'transparentModal',
'fullScreenModal',
'formSheet',
'pageSheet',
'containedModal',
'containedTransparentModal',
] as const) {
expect(isOverlayPresentation({ presentation: p })).toBe(true)
}
})
it('returns false for card and undefined', () => {
expect(isOverlayPresentation({ presentation: 'card' })).toBe(false)
expect(isOverlayPresentation({})).toBe(false)
expect(isOverlayPresentation(undefined)).toBe(false)
expect(isOverlayPresentation(null)).toBe(false)
})
})
describe('isTransparentOverlay', () => {
it('flags transparent variants only', () => {
expect(isTransparentOverlay({ presentation: 'transparentModal' })).toBe(true)
expect(isTransparentOverlay({ presentation: 'containedTransparentModal' })).toBe(true)
expect(isTransparentOverlay({ presentation: 'modal' })).toBe(false)
expect(isTransparentOverlay({ presentation: 'formSheet' })).toBe(false)
})
})
describe('convertStackStateToNonOverlayState', () => {
it('removes overlay routes and keeps card routes', () => {
const state = makeState(['home', 'detail', 'filter'], 2)
const descriptors = makeDescriptors(['home', 'detail', 'filter'], {
home: 'card',
detail: 'card',
filter: 'formSheet',
})
const out = convertStackStateToNonOverlayState(state, descriptors)
expect(out.routes.map((r) => r.name)).toEqual(['home', 'detail'])
})
it('falls back to last remaining route when active route was an overlay', () => {
const state = makeState(['home', 'detail', 'filter'], 2)
const descriptors = makeDescriptors(['home', 'detail', 'filter'], {
home: 'card',
detail: 'card',
filter: 'formSheet',
})
const out = convertStackStateToNonOverlayState(state, descriptors)
expect(out.index).toBe(1)
expect(out.routes[out.index]!.name).toBe('detail')
})
it('preserves active index when active route is not an overlay', () => {
const state = makeState(['home', 'detail', 'filter'], 1)
const descriptors = makeDescriptors(['home', 'detail', 'filter'], {
home: 'card',
detail: 'card',
filter: 'formSheet',
})
const out = convertStackStateToNonOverlayState(state, descriptors)
expect(out.routes[out.index]!.name).toBe('detail')
})
it('does not strip mid-stack overlays (regression: only the trailing overlay suffix is removed)', () => {
// User opened a sheet then navigated forward to a card. The sheet is
// sandwiched in the middle. It MUST stay in the underlying state so
// detail's previous-route / header-back context references the sheet.
const state = makeState(['home', 'sheet', 'detail'], 2)
const descriptors = makeDescriptors(['home', 'sheet', 'detail'], {
home: 'card',
sheet: 'formSheet',
detail: 'card',
})
const out = convertStackStateToNonOverlayState(state, descriptors)
expect(out.routes.map((r) => r.name)).toEqual(['home', 'sheet', 'detail'])
expect(out.index).toBe(2)
})
it('handles all-overlay state without crashing', () => {
const state = makeState(['only-sheet'], 0)
const descriptors = makeDescriptors(['only-sheet'], { 'only-sheet': 'formSheet' })
const out = convertStackStateToNonOverlayState(state, descriptors)
expect(out.routes).toHaveLength(0)
expect(out.index).toBe(0)
})
})
describe('findLastNonOverlayIndex', () => {
it('returns the highest index that is not an overlay', () => {
const state = makeState(['home', 'detail', 'filter', 'pop'], 3)
const descriptors = makeDescriptors(['home', 'detail', 'filter', 'pop'], {
home: 'card',
detail: 'card',
filter: 'formSheet',
pop: 'transparentModal',
})
expect(findLastNonOverlayIndex(state, descriptors)).toBe(1)
})
it('returns -1 when every route is an overlay', () => {
const state = makeState(['sheet'], 0)
const descriptors = makeDescriptors(['sheet'], { sheet: 'formSheet' })
expect(findLastNonOverlayIndex(state, descriptors)).toBe(-1)
})
})