first commit
This commit is contained in:
193
packages/core/src/runtime.test.ts
Normal file
193
packages/core/src/runtime.test.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { createApp, defineComponent, nextTick } from 'vue'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createNativeRouter, definePresentation, shouldCommitGesture } from './runtime'
|
||||
|
||||
const Page = defineComponent({ template: '<div>page</div>' })
|
||||
|
||||
async function harness(blockB: boolean | 'redirect' = false) {
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/a', component: Page },
|
||||
{ path: '/b', component: Page, meta: { native: { parent: '/a' } } },
|
||||
{ path: '/c', component: Page, meta: { native: { parent: '/a' } } },
|
||||
{ path: '/modal', component: Page, meta: { native: { presentation: 'sheet', parent: '/a' } } },
|
||||
{ path: '/left', component: Page, meta: { native: { siblingOrder: 0, siblingHistory: 'replace' } } },
|
||||
{ path: '/middle', component: Page, meta: { native: { siblingOrder: 1, siblingHistory: 'replace' } } },
|
||||
{ path: '/right', component: Page, meta: { native: { siblingOrder: 2, siblingHistory: 'replace' } } },
|
||||
],
|
||||
})
|
||||
if (blockB) router.beforeEach((to) => to.path === '/b' ? (blockB === 'redirect' ? '/modal' : false) : undefined)
|
||||
await router.push('/a')
|
||||
await router.isReady()
|
||||
const native = createNativeRouter({ router, cache: { maxInactive: 2 } })
|
||||
const app = createApp(Page)
|
||||
app.use(router)
|
||||
app.use(native)
|
||||
await nextTick()
|
||||
return { router, native }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: true } as MediaQueryList)
|
||||
})
|
||||
|
||||
describe('gesture decisions', () => {
|
||||
it('uses progress or a deliberate velocity to commit', () => {
|
||||
expect(shouldCommitGesture(0.4, 0)).toBe(true)
|
||||
expect(shouldCommitGesture(0.12, 0.7)).toBe(true)
|
||||
expect(shouldCommitGesture(0.04, 1.4)).toBe(false)
|
||||
expect(shouldCommitGesture(0.2, 0.1)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('native router transactions', () => {
|
||||
it('preloads a target without changing URL history', async () => {
|
||||
const { router, native } = await harness()
|
||||
const id = await native.beginInteractive('push', '/b')
|
||||
expect(id).not.toBeNull()
|
||||
expect(router.currentRoute.value.path).toBe('/a')
|
||||
expect(native.entries.value.some((entry) => entry.status === 'preview')).toBe(true)
|
||||
await native.cancelInteractive()
|
||||
expect(router.currentRoute.value.path).toBe('/a')
|
||||
expect(native.transaction.value).toBeNull()
|
||||
})
|
||||
|
||||
it('commits a loaded preview through Vue Router', async () => {
|
||||
const { router, native } = await harness()
|
||||
await native.beginInteractive('push', '/b')
|
||||
native.updateInteractive(0.55, 0.1)
|
||||
expect(await native.finishInteractive()).toBe(true)
|
||||
expect(router.currentRoute.value.path).toBe('/b')
|
||||
expect(native.entries.value.filter((entry) => entry.status === 'active')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('snaps back and removes the preview when a guard rejects commit', async () => {
|
||||
const { router, native } = await harness(true)
|
||||
await native.beginInteractive('push', '/b')
|
||||
native.updateInteractive(0.8, 0)
|
||||
expect(await native.finishInteractive()).toBe(false)
|
||||
expect(router.currentRoute.value.path).toBe('/a')
|
||||
expect(native.entries.value.some((entry) => entry.route.path === '/b')).toBe(false)
|
||||
})
|
||||
|
||||
it('uses declared parents for cold-start predictive back', async () => {
|
||||
const { router, native } = await harness()
|
||||
await native.replace('/b', { presentation: 'none' })
|
||||
const transaction = await native.beginInteractive('pop')
|
||||
expect(transaction).not.toBeNull()
|
||||
expect(native.transaction.value?.direction).toBe('back')
|
||||
await native.cancelInteractive()
|
||||
expect(router.currentRoute.value.path).toBe('/b')
|
||||
})
|
||||
|
||||
it('discards the stale preview when Vue Router redirects a commit', async () => {
|
||||
const { router, native } = await harness('redirect')
|
||||
await native.beginInteractive('push', '/b')
|
||||
expect(await native.finishInteractive(true)).toBe(true)
|
||||
expect(router.currentRoute.value.path).toBe('/modal')
|
||||
expect(native.entries.value.some((entry) => entry.status === 'preview')).toBe(false)
|
||||
})
|
||||
|
||||
it('registers application-defined presentations', async () => {
|
||||
const { native } = await harness()
|
||||
const presentation = definePresentation({ name: 'flip', axis: 'x', layerStyle: () => ({ opacity: 0.5 }) })
|
||||
native.registerPresentation(presentation)
|
||||
expect(native.presentationFor('flip')).toBe(presentation)
|
||||
})
|
||||
|
||||
it('treats navigation to the active route as a strict no-op', async () => {
|
||||
const { router, native } = await harness()
|
||||
const entries = [...native.entries.value]
|
||||
expect(await native.push('/a')).toBe(false)
|
||||
expect(await native.replace('/a')).toBe(false)
|
||||
expect(router.currentRoute.value.path).toBe('/a')
|
||||
expect(native.transaction.value).toBeNull()
|
||||
expect(native.entries.value).toEqual(entries)
|
||||
})
|
||||
|
||||
it('derives sibling direction from route order and uses adjacent-page motion', async () => {
|
||||
const { native } = await harness()
|
||||
await native.replace('/middle', { presentation: 'none' })
|
||||
|
||||
await native.beginInteractive('sibling', '/left', { replace: true })
|
||||
expect(native.transaction.value).toMatchObject({ direction: 'back', presentation: 'slide' })
|
||||
await native.cancelInteractive()
|
||||
|
||||
await native.beginInteractive('sibling', '/right', { replace: true })
|
||||
expect(native.transaction.value).toMatchObject({ direction: 'forward', presentation: 'slide' })
|
||||
await native.cancelInteractive()
|
||||
})
|
||||
|
||||
it('keeps replaced sibling views cached but out of the back stack', async () => {
|
||||
const { native } = await harness()
|
||||
await native.replace('/left', { presentation: 'none' })
|
||||
await native.sibling('/middle', { replace: true })
|
||||
await native.push('/c')
|
||||
|
||||
await native.beginInteractive('pop')
|
||||
const target = native.entries.value.find((entry) => entry.key === native.transaction.value?.toKey)
|
||||
expect(target?.route.path).toBe('/middle')
|
||||
expect(target?.route.path).not.toBe('/left')
|
||||
await native.cancelInteractive()
|
||||
})
|
||||
|
||||
it('does not preview a stale forward entry after pop then push', async () => {
|
||||
const { native } = await harness()
|
||||
await native.push('/b')
|
||||
await native.pop()
|
||||
await native.push('/c')
|
||||
|
||||
await native.beginInteractive('pop')
|
||||
const target = native.entries.value.find((entry) => entry.key === native.transaction.value?.toKey)
|
||||
expect(target?.route.path).toBe('/a')
|
||||
await native.cancelInteractive()
|
||||
})
|
||||
|
||||
it('dismisses with the presented route animation regardless of the route below it', async () => {
|
||||
const { native } = await harness()
|
||||
await native.push('/b')
|
||||
await native.present('/modal', 'sheet')
|
||||
|
||||
await native.beginInteractive('dismiss')
|
||||
expect(native.transaction.value).toMatchObject({ direction: 'back', presentation: 'sheet' })
|
||||
await native.cancelInteractive()
|
||||
})
|
||||
|
||||
it('refuses to overlap a second transaction with an active gesture', async () => {
|
||||
const { native } = await harness()
|
||||
const first = await native.beginInteractive('push', '/b')
|
||||
expect(await native.beginInteractive('push', '/c')).toBeNull()
|
||||
expect(native.transaction.value?.id).toBe(first)
|
||||
await native.cancelInteractive()
|
||||
})
|
||||
|
||||
it('queues imperative navigation until the current transition settles', async () => {
|
||||
const { router, native } = await harness()
|
||||
await native.beginInteractive('push', '/b')
|
||||
const finishing = native.finishInteractive(true)
|
||||
const queued = native.push('/c')
|
||||
expect(await finishing).toBe(true)
|
||||
expect(await queued).toBe(true)
|
||||
expect(router.currentRoute.value.path).toBe('/c')
|
||||
expect(native.transaction.value).toBeNull()
|
||||
})
|
||||
|
||||
it('reconciles direct browser back navigation with the native stack', async () => {
|
||||
const { router, native } = await harness()
|
||||
await native.push('/b')
|
||||
const navigated = new Promise<void>((resolve) => {
|
||||
const remove = router.afterEach(() => {
|
||||
remove()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
router.back()
|
||||
await navigated
|
||||
|
||||
expect(router.currentRoute.value.path).toBe('/a')
|
||||
expect(native.canGoBack.value).toBe(false)
|
||||
expect(await native.beginInteractive('pop')).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user