Add prettier. Format.
This commit is contained in:
+413
-304
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user