feat: expose stable journey intent signals
This commit is contained in:
@@ -28,6 +28,11 @@ const journey = (
|
||||
chapter: "infrastructure",
|
||||
sign: null,
|
||||
arrived: false,
|
||||
sectionActivation: {
|
||||
layoutRevision: 0,
|
||||
activeHeadingOffsetVh: 0,
|
||||
nextHeadingOffsetVh: null,
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
canonicalSectionIdFromHash,
|
||||
createNavigationController,
|
||||
validateSectionAnchor,
|
||||
type NavigationState,
|
||||
type NavigationRuntime,
|
||||
} from "@/journey/navigation-controller";
|
||||
import type { SectionId } from "@/content/types";
|
||||
@@ -205,6 +206,7 @@ describe("navigation epochs and effects", () => {
|
||||
expect(controller.state()).toEqual({
|
||||
epoch: 2,
|
||||
source: "passive-scroll",
|
||||
phase: "idle",
|
||||
destinationId: null,
|
||||
signDestinationId: null,
|
||||
});
|
||||
@@ -221,6 +223,30 @@ describe("navigation epochs and effects", () => {
|
||||
expect(harness.focused).toEqual(["experience"]);
|
||||
});
|
||||
|
||||
it("publishes travelling and settled only for the latest explicit epoch", () => {
|
||||
const harness = createRuntime(true);
|
||||
const controller = createNavigationController(sectionIds, harness.runtime);
|
||||
const states: NavigationState[] = [];
|
||||
controller.subscribe((state) => states.push(state));
|
||||
|
||||
controller.navigate("contact", target("contact"));
|
||||
controller.navigate("toolkit", target("toolkit"));
|
||||
expect(controller.state()).toMatchObject({
|
||||
epoch: 2,
|
||||
source: "explicit-navigation",
|
||||
phase: "travelling",
|
||||
destinationId: "toolkit",
|
||||
});
|
||||
|
||||
harness.emitScrollEnd();
|
||||
expect(controller.state()).toMatchObject({
|
||||
epoch: 2,
|
||||
phase: "settled",
|
||||
destinationId: "toolkit",
|
||||
});
|
||||
expect(states.filter((state) => state.phase === "settled")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("falls back to three consecutive sub-pixel-stable animation frames", () => {
|
||||
const harness = createRuntime(false);
|
||||
const controller = createNavigationController(sectionIds, harness.runtime);
|
||||
@@ -271,7 +297,9 @@ describe("navigation epochs and effects", () => {
|
||||
const controller = createNavigationController(sectionIds, harness.runtime);
|
||||
|
||||
expect(controller.initialize("toolkit", target("toolkit"))).toBe(1);
|
||||
expect(controller.state()).toMatchObject({ phase: "settled" });
|
||||
expect(controller.restore("profile", target("profile"))).toBe(2);
|
||||
expect(controller.state()).toMatchObject({ phase: "travelling" });
|
||||
harness.runFrame();
|
||||
expect(harness.pushes).toEqual([]);
|
||||
expect(harness.replacements).toEqual([]);
|
||||
@@ -283,6 +311,7 @@ describe("navigation epochs and effects", () => {
|
||||
expect(controller.state()).toEqual({
|
||||
epoch: 2,
|
||||
source: "history-restoration",
|
||||
phase: "settled",
|
||||
destinationId: "profile",
|
||||
signDestinationId: null,
|
||||
});
|
||||
|
||||
@@ -582,6 +582,11 @@ function journey(overrides: Partial<JourneySnapshot> = {}): JourneySnapshot {
|
||||
chapter: 'deep-night',
|
||||
sign: null,
|
||||
arrived: false,
|
||||
sectionActivation: {
|
||||
layoutRevision: 0,
|
||||
activeHeadingOffsetVh: 0,
|
||||
nextHeadingOffsetVh: null,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -465,6 +465,7 @@ describe('useScrollDrive', () => {
|
||||
const navigation = shallowRef({
|
||||
epoch: 0,
|
||||
source: 'passive-scroll' as const,
|
||||
phase: 'idle' as const,
|
||||
destinationId: null,
|
||||
signDestinationId: null,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
activationLineDocumentY,
|
||||
deriveSectionActivationSnapshot,
|
||||
deriveSectionState,
|
||||
isAtDocumentEnd,
|
||||
selectActiveSection,
|
||||
@@ -109,6 +110,41 @@ describe("active section selection", () => {
|
||||
).toBe("profile");
|
||||
});
|
||||
|
||||
it("derives responsive heading offsets from the same activation-line geometry", () => {
|
||||
expect(deriveSectionActivationSnapshot({
|
||||
headings: [
|
||||
{ id: "start", top: 100 },
|
||||
{ id: "profile", top: 420 },
|
||||
{ id: "experience", top: 1_120 },
|
||||
],
|
||||
activeSectionId: "profile",
|
||||
activationY: 500,
|
||||
viewportHeight: 800,
|
||||
layoutRevision: 7,
|
||||
})).toEqual({
|
||||
layoutRevision: 7,
|
||||
activeHeadingOffsetVh: -0.1,
|
||||
nextHeadingOffsetVh: 0.775,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns no following-heading offset for the final active heading", () => {
|
||||
expect(deriveSectionActivationSnapshot({
|
||||
headings: [
|
||||
{ id: "start", top: 100 },
|
||||
{ id: "profile", top: 420 },
|
||||
],
|
||||
activeSectionId: "profile",
|
||||
activationY: 500,
|
||||
viewportHeight: 800,
|
||||
layoutRevision: 8,
|
||||
})).toEqual({
|
||||
layoutRevision: 8,
|
||||
activeHeadingOffsetVh: -0.1,
|
||||
nextHeadingOffsetVh: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("maps every canonical section to its chapter and arrival state", () => {
|
||||
expect([
|
||||
deriveSectionState("start"),
|
||||
|
||||
Reference in New Issue
Block a user