Add prettier. Format.

This commit is contained in:
2026-07-22 02:12:05 +00:00
parent d28b09dc21
commit 18baa96848
45 changed files with 4520 additions and 2613 deletions
+413 -304
View File
@@ -1,372 +1,481 @@
import { createApp, defineComponent, nextTick } from 'vue'
import { createMemoryHistory, createRouter } from 'vue-router'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, nextTick } from "vue";
import { createMemoryHistory, createRouter } from "vue-router";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
createNativeRouter,
definePresentation,
shouldCommitGesture,
springTimeScaleForVelocity,
} from './runtime'
import { createNativeNavigationProfiler } from './profiler'
} from "./runtime";
import { createNativeNavigationProfiler } from "./profiler";
const Page = defineComponent({ template: '<div>page</div>' })
const Page = defineComponent({ template: "<div>page</div>" });
async function harness(blockB: boolean | 'redirect' = false) {
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' } } },
{ path: '/no-cache', component: Page, meta: { native: { cache: false, parent: '/a' } } },
{ path: '/pinned', component: Page, meta: { native: { cache: 'pin', parent: '/a' } } },
{ path: '/item/:id', component: Page, meta: { native: { parent: '/a' } } },
{ 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" } },
},
{
path: "/no-cache",
component: Page,
meta: { native: { cache: false, parent: "/a" } },
},
{
path: "/pinned",
component: Page,
meta: { native: { cache: "pin", parent: "/a" } },
},
{
path: "/item/:id",
component: Page,
meta: { native: { parent: "/a" } },
},
],
})
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 }
});
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)
})
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, 1.4)).toBe(true)
expect(shouldCommitGesture(0.04, 4)).toBe(false)
expect(shouldCommitGesture(0.2, 0.4)).toBe(false)
})
describe("gesture decisions", () => {
it("uses progress or a deliberate velocity to commit", () => {
expect(shouldCommitGesture(0.4, 0)).toBe(true);
expect(shouldCommitGesture(0.12, 1.4)).toBe(true);
expect(shouldCommitGesture(0.04, 4)).toBe(false);
expect(shouldCommitGesture(0.2, 0.4)).toBe(false);
});
it('settles a fast flick more quickly without unbounded spring steps', () => {
expect(springTimeScaleForVelocity(0)).toBe(1)
expect(springTimeScaleForVelocity(2)).toBeCloseTo(1.6)
expect(springTimeScaleForVelocity(8)).toBe(3)
expect(springTimeScaleForVelocity(-20)).toBe(3)
})
})
it("settles a fast flick more quickly without unbounded spring steps", () => {
expect(springTimeScaleForVelocity(0)).toBe(1);
expect(springTimeScaleForVelocity(2)).toBeCloseTo(1.6);
expect(springTimeScaleForVelocity(8)).toBe(3);
expect(springTimeScaleForVelocity(-20)).toBe(3);
});
});
describe('native router transactions', () => {
it('exports opt-in frame diagnostics without route params or query values', async () => {
const { native } = await harness()
const profiler = createNativeNavigationProfiler(native, { metadata: { build: 'test' } })
profiler.start()
describe("native router transactions", () => {
it("exports opt-in frame diagnostics without route params or query values", async () => {
const { native } = await harness();
const profiler = createNativeNavigationProfiler(native, {
metadata: { build: "test" },
});
profiler.start();
await native.push('/item/private-id?token=secret')
const report = profiler.stop()
await native.push("/item/private-id?token=secret");
const report = profiler.stop();
expect(report.schema).toBe('native-vue-router-profile@1')
expect(report.metadata).toEqual({ build: 'test' })
expect(report.events.map((event) => event.type)).toEqual(expect.arrayContaining([
'route-load-start',
'route-load-end',
'transaction-start',
'transaction-end',
]))
expect(report.transactions).toMatchObject([{ route: '/item/:id', cold: true, outcome: 'committed' }])
expect(profiler.toJSON(report)).not.toContain('private-id')
expect(profiler.toJSON(report)).not.toContain('secret')
profiler.dispose()
})
expect(report.schema).toBe("native-vue-router-profile@1");
expect(report.metadata).toEqual({ build: "test" });
expect(report.events.map((event) => event.type)).toEqual(
expect.arrayContaining([
"route-load-start",
"route-load-end",
"transaction-start",
"transaction-end",
]),
);
expect(report.transactions).toMatchObject([
{ route: "/item/:id", cold: true, outcome: "committed" },
]);
expect(profiler.toJSON(report)).not.toContain("private-id");
expect(profiler.toJSON(report)).not.toContain("secret");
profiler.dispose();
});
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("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("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("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("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("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("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("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' })
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", "/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()
})
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')
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()
})
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('creates sibling views lazily and retains visited replace-style siblings', async () => {
const { native } = await harness()
expect(native.entries.value.map((entry) => entry.route.path)).toEqual(['/a'])
it("creates sibling views lazily and retains visited replace-style siblings", async () => {
const { native } = await harness();
expect(native.entries.value.map((entry) => entry.route.path)).toEqual([
"/a",
]);
await native.replace('/left', { presentation: 'none' })
expect(native.entries.value.some((entry) => entry.route.path === '/middle')).toBe(false)
await native.sibling('/middle', { replace: true })
await native.replace("/left", { presentation: "none" });
expect(
native.entries.value.some((entry) => entry.route.path === "/middle"),
).toBe(false);
await native.sibling("/middle", { replace: true });
expect(native.entries.value.find((entry) => entry.route.path === '/left')).toMatchObject({ mounted: true, status: 'inactive' })
expect(native.entries.value.find((entry) => entry.route.path === '/middle')).toMatchObject({ mounted: true, status: 'active' })
expect(native.entries.value.some((entry) => entry.route.path === '/right')).toBe(false)
})
expect(
native.entries.value.find((entry) => entry.route.path === "/left"),
).toMatchObject({ mounted: true, status: "inactive" });
expect(
native.entries.value.find((entry) => entry.route.path === "/middle"),
).toMatchObject({ mounted: true, status: "active" });
expect(
native.entries.value.some((entry) => entry.route.path === "/right"),
).toBe(false);
});
it('prepares a newly mounted destination for a paint before animation can begin', async () => {
vi.mocked(window.matchMedia).mockReturnValue({ matches: false } as MediaQueryList)
let paint: FrameRequestCallback | undefined
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
paint = callback
return 1
})
const { native } = await harness()
it("prepares a newly mounted destination for a paint before animation can begin", async () => {
vi.mocked(window.matchMedia).mockReturnValue({
matches: false,
} as MediaQueryList);
let paint: FrameRequestCallback | undefined;
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
paint = callback;
return 1;
});
const { native } = await harness();
let resolved = false
const beginning = native.beginInteractive('push', '/b').then((id) => {
resolved = true
return id
})
await vi.waitFor(() => expect(paint).toBeTypeOf('function'))
let resolved = false;
const beginning = native.beginInteractive("push", "/b").then((id) => {
resolved = true;
return id;
});
await vi.waitFor(() => expect(paint).toBeTypeOf("function"));
expect(resolved).toBe(false)
expect(native.entries.value.find((entry) => entry.route.path === '/b')?.mounted).toBe(true)
paint?.(performance.now())
expect(await beginning).not.toBeNull()
vi.mocked(window.matchMedia).mockReturnValue({ matches: true } as MediaQueryList)
await native.cancelInteractive()
})
expect(resolved).toBe(false);
expect(
native.entries.value.find((entry) => entry.route.path === "/b")?.mounted,
).toBe(true);
paint?.(performance.now());
expect(await beginning).not.toBeNull();
vi.mocked(window.matchMedia).mockReturnValue({
matches: true,
} as MediaQueryList);
await native.cancelInteractive();
});
it('collapses pushed history when a tab replaces it with an existing root', async () => {
const { router, native } = await harness()
await native.push('/b')
expect(native.canGoBack.value).toBe(true)
it("collapses pushed history when a tab replaces it with an existing root", async () => {
const { router, native } = await harness();
await native.push("/b");
expect(native.canGoBack.value).toBe(true);
await native.sibling('/a', { replace: true })
await native.sibling("/a", { replace: true });
expect(router.currentRoute.value.path).toBe('/a')
expect(native.canGoBack.value).toBe(false)
expect(await native.beginInteractive('pop')).toBeNull()
})
expect(router.currentRoute.value.path).toBe("/a");
expect(native.canGoBack.value).toBe(false);
expect(await native.beginInteractive("pop")).toBeNull();
});
it('manually unloads inactive route instances but never the active view', async () => {
const { native } = await harness()
await native.replace('/left', { presentation: 'none' })
await native.sibling('/middle', { replace: true })
it("manually unloads inactive route instances but never the active view", async () => {
const { native } = await harness();
await native.replace("/left", { presentation: "none" });
await native.sibling("/middle", { replace: true });
expect(native.unload('/left')).toBe(1)
expect(native.entries.value.find((entry) => entry.route.path === '/left')).toMatchObject({
expect(native.unload("/left")).toBe(1);
expect(
native.entries.value.find((entry) => entry.route.path === "/left"),
).toMatchObject({
mounted: false,
evictionReason: 'manual',
})
expect(native.unload('/middle')).toBe(0)
expect(native.entries.value.find((entry) => entry.route.path === '/middle')?.mounted).toBe(true)
})
evictionReason: "manual",
});
expect(native.unload("/middle")).toBe(0);
expect(
native.entries.value.find((entry) => entry.route.path === "/middle")
?.mounted,
).toBe(true);
});
it('evicts the least-recently-used inactive view when the cache limit is exceeded', async () => {
let clock = 0
vi.spyOn(performance, 'now').mockImplementation(() => ++clock)
const { native } = await harness()
await native.replace('/left', { presentation: 'none' })
await native.sibling('/middle', { replace: true })
await native.sibling('/right', { replace: true })
await native.sibling('/a', { replace: true })
it("evicts the least-recently-used inactive view when the cache limit is exceeded", async () => {
let clock = 0;
vi.spyOn(performance, "now").mockImplementation(() => ++clock);
const { native } = await harness();
await native.replace("/left", { presentation: "none" });
await native.sibling("/middle", { replace: true });
await native.sibling("/right", { replace: true });
await native.sibling("/a", { replace: true });
expect(native.entries.value.find((entry) => entry.route.path === '/left')).toMatchObject({
expect(
native.entries.value.find((entry) => entry.route.path === "/left"),
).toMatchObject({
mounted: false,
evictionReason: 'cache-limit',
})
expect(native.entries.value.find((entry) => entry.route.path === '/middle')?.mounted).toBe(true)
expect(native.entries.value.find((entry) => entry.route.path === '/right')?.mounted).toBe(true)
expect(native.cacheStats.value.inactive).toBe(2)
})
evictionReason: "cache-limit",
});
expect(
native.entries.value.find((entry) => entry.route.path === "/middle")
?.mounted,
).toBe(true);
expect(
native.entries.value.find((entry) => entry.route.path === "/right")
?.mounted,
).toBe(true);
expect(native.cacheStats.value.inactive).toBe(2);
});
it('evicts a pushed route after it is popped out of history', async () => {
const { native } = await harness()
await native.push('/b')
const pushedKey = native.activeKey.value
await native.pop()
it("evicts a pushed route after it is popped out of history", async () => {
const { native } = await harness();
await native.push("/b");
const pushedKey = native.activeKey.value;
await native.pop();
expect(native.entries.value.find((entry) => entry.key === pushedKey)).toMatchObject({
expect(
native.entries.value.find((entry) => entry.key === pushedKey),
).toMatchObject({
mounted: false,
status: 'evicted',
evictionReason: 'popped',
})
})
status: "evicted",
evictionReason: "popped",
});
});
it('honors cache opt-out even for a route that remains in back history', async () => {
const { native } = await harness()
await native.push('/no-cache')
const noCacheKey = native.activeKey.value
await native.push('/c')
it("honors cache opt-out even for a route that remains in back history", async () => {
const { native } = await harness();
await native.push("/no-cache");
const noCacheKey = native.activeKey.value;
await native.push("/c");
expect(native.entries.value.find((entry) => entry.key === noCacheKey)).toMatchObject({
expect(
native.entries.value.find((entry) => entry.key === noCacheKey),
).toMatchObject({
mounted: false,
evictionReason: 'cache-disabled',
})
expect(await native.beginInteractive('pop')).not.toBeNull()
expect(native.entries.value.find((entry) => entry.key === noCacheKey)?.mounted).toBe(true)
await native.cancelInteractive()
})
evictionReason: "cache-disabled",
});
expect(await native.beginInteractive("pop")).not.toBeNull();
expect(
native.entries.value.find((entry) => entry.key === noCacheKey)?.mounted,
).toBe(true);
await native.cancelInteractive();
});
it('keeps pinned views during normal trims and releases them when requested', async () => {
const { native } = await harness()
await native.replace('/pinned', { presentation: 'none' })
const pinnedKey = native.activeKey.value
await native.push('/c')
it("keeps pinned views during normal trims and releases them when requested", async () => {
const { native } = await harness();
await native.replace("/pinned", { presentation: "none" });
const pinnedKey = native.activeKey.value;
await native.push("/c");
native.trimCache()
expect(native.entries.value.find((entry) => entry.key === pinnedKey)?.mounted).toBe(true)
native.trimCache({ includePinned: true })
expect(native.entries.value.find((entry) => entry.key === pinnedKey)).toMatchObject({
native.trimCache();
expect(
native.entries.value.find((entry) => entry.key === pinnedKey)?.mounted,
).toBe(true);
native.trimCache({ includePinned: true });
expect(
native.entries.value.find((entry) => entry.key === pinnedKey),
).toMatchObject({
mounted: false,
evictionReason: 'trimmed',
})
})
evictionReason: "trimmed",
});
});
it('evicts a previously cached target when its guard rejects re-entry', async () => {
const { router, native } = await harness()
await native.replace('/left', { presentation: 'none' })
await native.sibling('/middle', { replace: true })
const cachedLeft = native.entries.value.find((entry) => entry.route.path === '/left')
expect(cachedLeft?.mounted).toBe(true)
const removeGuard = router.beforeEach((to) => to.path === '/left' ? false : undefined)
it("evicts a previously cached target when its guard rejects re-entry", async () => {
const { router, native } = await harness();
await native.replace("/left", { presentation: "none" });
await native.sibling("/middle", { replace: true });
const cachedLeft = native.entries.value.find(
(entry) => entry.route.path === "/left",
);
expect(cachedLeft?.mounted).toBe(true);
const removeGuard = router.beforeEach((to) =>
to.path === "/left" ? false : undefined,
);
expect(await native.sibling('/left', { replace: true })).toBe(false)
expect(native.entries.value.find((entry) => entry.key === cachedLeft?.key)).toMatchObject({
expect(await native.sibling("/left", { replace: true })).toBe(false);
expect(
native.entries.value.find((entry) => entry.key === cachedLeft?.key),
).toMatchObject({
mounted: false,
evictionReason: 'navigation-rejected',
})
expect(native.cacheStats.value.totalEvictions).toBeGreaterThan(0)
removeGuard()
})
evictionReason: "navigation-rejected",
});
expect(native.cacheStats.value.totalEvictions).toBeGreaterThan(0);
removeGuard();
});
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')
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()
})
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')
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()
})
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("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('accepts imperative navigation as the previous transition finalizes', 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("accepts imperative navigation as the previous transition finalizes", 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')
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
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()
})
})
expect(router.currentRoute.value.path).toBe("/a");
expect(native.canGoBack.value).toBe(false);
expect(await native.beginInteractive("pop")).toBeNull();
});
});