174 lines
5.0 KiB
TypeScript
174 lines
5.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
activationLineDocumentY,
|
|
deriveSectionActivationSnapshot,
|
|
deriveSectionState,
|
|
isAtDocumentEnd,
|
|
selectActiveSection,
|
|
type SectionHeadingGeometry,
|
|
} from "@/journey/section-controller";
|
|
|
|
const headings: readonly SectionHeadingGeometry[] = [
|
|
{ id: "start", top: 100 },
|
|
{ id: "profile", top: 350 },
|
|
{ id: "experience", top: 700 },
|
|
{ id: "infrastructure", top: 1100 },
|
|
{ id: "selected-builds", top: 1500 },
|
|
{ id: "additional-work", top: 1900 },
|
|
{ id: "toolkit", top: 2300 },
|
|
{ id: "contact", top: 2700 },
|
|
];
|
|
|
|
describe("active section selection", () => {
|
|
it("places the activation line at a literal 35 percent of the visual viewport", () => {
|
|
expect(
|
|
activationLineDocumentY({
|
|
scrollY: 200,
|
|
visualViewportOffsetTop: 25,
|
|
visualViewportHeight: 1000,
|
|
}),
|
|
).toBe(575);
|
|
});
|
|
|
|
it("forces document end within exactly two CSS pixels using current geometry", () => {
|
|
expect(
|
|
isAtDocumentEnd({
|
|
scrollY: 498,
|
|
scrollHeight: 1000,
|
|
visualViewportHeight: 500,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isAtDocumentEnd({
|
|
scrollY: 497.999,
|
|
scrollHeight: 1000,
|
|
visualViewportHeight: 500,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isAtDocumentEnd({
|
|
scrollY: 498,
|
|
scrollHeight: 1200,
|
|
visualViewportHeight: 500,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("keeps the first section before any heading reaches the activation line", () => {
|
|
expect(
|
|
selectActiveSection({ headings, activationY: 99, atDocumentEnd: false }),
|
|
).toBe("start");
|
|
});
|
|
|
|
it("selects the last heading at or above the literal 35 percent activation line", () => {
|
|
expect(
|
|
selectActiveSection({
|
|
headings,
|
|
activationY: 699.999,
|
|
atDocumentEnd: false,
|
|
}),
|
|
).toBe("profile");
|
|
expect(
|
|
selectActiveSection({ headings, activationY: 700, atDocumentEnd: false }),
|
|
).toBe("experience");
|
|
});
|
|
|
|
it("lets later document order win when heading positions are identical", () => {
|
|
expect(
|
|
selectActiveSection({
|
|
headings: [
|
|
{ id: "start", top: 0 },
|
|
{ id: "profile", top: 350 },
|
|
{ id: "experience", top: 350 },
|
|
],
|
|
activationY: 350,
|
|
atDocumentEnd: false,
|
|
}),
|
|
).toBe("experience");
|
|
});
|
|
|
|
it("forces the final canonical section at document end", () => {
|
|
expect(
|
|
selectActiveSection({ headings, activationY: 1400, atDocumentEnd: true }),
|
|
).toBe("contact");
|
|
});
|
|
|
|
it("uses fresh late-growth geometry on every selection", () => {
|
|
const grown = headings.map((heading) =>
|
|
heading.id === "experience" ? { ...heading, top: 900 } : heading,
|
|
);
|
|
|
|
expect(
|
|
selectActiveSection({ headings, activationY: 750, atDocumentEnd: false }),
|
|
).toBe("experience");
|
|
expect(
|
|
selectActiveSection({
|
|
headings: grown,
|
|
activationY: 750,
|
|
atDocumentEnd: false,
|
|
}),
|
|
).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"),
|
|
deriveSectionState("profile"),
|
|
deriveSectionState("experience"),
|
|
deriveSectionState("infrastructure"),
|
|
deriveSectionState("selected-builds"),
|
|
deriveSectionState("additional-work"),
|
|
deriveSectionState("toolkit"),
|
|
deriveSectionState("contact"),
|
|
]).toEqual([
|
|
{ activeSectionId: "start", chapter: "deep-night", arrived: false },
|
|
{ activeSectionId: "profile", chapter: "deep-night", arrived: false },
|
|
{ activeSectionId: "experience", chapter: "systems", arrived: false },
|
|
{
|
|
activeSectionId: "infrastructure",
|
|
chapter: "infrastructure",
|
|
arrived: false,
|
|
},
|
|
{ activeSectionId: "selected-builds", chapter: "builds", arrived: false },
|
|
{ activeSectionId: "additional-work", chapter: "builds", arrived: false },
|
|
{ activeSectionId: "toolkit", chapter: "pre-dawn", arrived: false },
|
|
{ activeSectionId: "contact", chapter: "destination", arrived: true },
|
|
]);
|
|
});
|
|
});
|