245 lines · 8.2 KB · TypeScript
RawDownload
import { describe, expect, it, vi } from "vite-plus/test"; import {  getVisibleSettingsSectionIds,  observeSettingsSectionVisibility,  type SettingsSectionVisibilityEnvironment,} from "./settingsSectionVisibility"; type VisibilityEntry = Pick<  IntersectionObserverEntry,  "intersectionRatio" | "isIntersecting" | "target">; function createHarness(  targetIds: ReadonlyArray<string>,  initialRoot: Element | null = { name: "initial-root" } as unknown as Element,) {  const targets = new Map(    targetIds.map((targetId) => [targetId, { targetId } as unknown as Element]),  );  const observed = new Set<Element>();  const unobserve = vi.fn((target: Element) => observed.delete(target));  const disconnectIntersections = vi.fn();  const disconnectMutations = vi.fn();  const intersectionCallbacks: Array<(entries: ReadonlyArray<VisibilityEntry>) => void> = [];  const intersectionRoots: Element[] = [];  let root = initialRoot;  let onMutation = () => {};   const environment: SettingsSectionVisibilityEnvironment = {    findRoot: () => root,    findTarget: (_root, targetId) => targets.get(targetId) ?? null,    createIntersectionObserver(callback, observedRoot) {      intersectionCallbacks.push(callback);      intersectionRoots.push(observedRoot);      return {        observe: (target) => observed.add(target),        unobserve,        disconnect: disconnectIntersections,      };    },    createMutationObserver(callback) {      onMutation = callback;      return { disconnect: disconnectMutations };    },  };   return {    environment,    targets,    observed,    unobserve,    disconnectIntersections,    disconnectMutations,    intersectionCallbacks,    intersectionRoots,    intersect(entries: ReadonlyArray<VisibilityEntry>) {      intersectionCallbacks.at(-1)?.(entries);    },    mutate() {      onMutation();    },    replaceRoot(nextRoot: Element | null) {      root = nextRoot;      onMutation();    },  };} function visibleEntry(  target: Element,  { isIntersecting = true, intersectionRatio = 1 } = {},): VisibilityEntry {  return { target, isIntersecting, intersectionRatio };} describe("settings section visibility", () => {  it("does not reuse visibility when returning to the same sectioned route", () => {    const firstGeneralVisit = { path: "/settings/general" };    const firstVisibility = {      scope: firstGeneralVisit,      targetIds: new Set(["text-generation"]),    };     expect(      getVisibleSettingsSectionIds({        activePath: "/settings/general",        scope: firstGeneralVisit,        visibility: firstVisibility,      }),    ).toEqual(new Set(["text-generation"]));    expect(      getVisibleSettingsSectionIds({        activePath: "/settings/providers",        scope: null,        visibility: firstVisibility,      }),    ).toEqual(new Set());     const secondGeneralVisit = { path: "/settings/general" };    expect(      getVisibleSettingsSectionIds({        activePath: "/settings/general",        scope: secondGeneralVisit,        visibility: firstVisibility,      }),    ).toEqual(new Set());  });   it("accumulates visible sections and emits them in sidebar order", () => {    const harness = createHarness(["one", "two", "three"]);    const emissions: ReadonlyArray<string>[] = [];    observeSettingsSectionVisibility({      container: {} as Element,      targetIds: ["one", "two", "three"],      onChange: (visible) => emissions.push(visible),      environment: harness.environment,    });     harness.intersect([visibleEntry(harness.targets.get("two")!)]);    harness.intersect([visibleEntry(harness.targets.get("one")!)]);     expect(emissions).toEqual([[], ["two"], ["one", "two"]]);  });   it("treats zero-ratio and non-intersecting entries as hidden", () => {    const harness = createHarness(["one", "two"]);    const emissions: ReadonlyArray<string>[] = [];    observeSettingsSectionVisibility({      container: {} as Element,      targetIds: ["one", "two"],      onChange: (visible) => emissions.push(visible),      environment: harness.environment,    });     const one = harness.targets.get("one")!;    const two = harness.targets.get("two")!;    harness.intersect([visibleEntry(one), visibleEntry(two)]);    harness.intersect([visibleEntry(one, { intersectionRatio: 0 })]);    harness.intersect([visibleEntry(two, { isIntersecting: false })]);     expect(emissions).toEqual([[], ["one", "two"], ["two"], []]);  });   it("resyncs replaced and removed targets without retaining stale visibility", () => {    const harness = createHarness(["dynamic"]);    const emissions: ReadonlyArray<string>[] = [];    observeSettingsSectionVisibility({      container: {} as Element,      targetIds: ["dynamic"],      onChange: (visible) => emissions.push(visible),      environment: harness.environment,    });     const firstTarget = harness.targets.get("dynamic")!;    harness.intersect([visibleEntry(firstTarget)]);    const replacementTarget = { targetId: "dynamic-replacement" } as unknown as Element;    harness.targets.set("dynamic", replacementTarget);    harness.mutate();     expect(harness.unobserve).toHaveBeenCalledWith(firstTarget);    expect(harness.observed.has(replacementTarget)).toBe(true);    expect(emissions.at(-1)).toEqual([]);     harness.intersect([visibleEntry(firstTarget)]);    expect(emissions.at(-1)).toEqual([]);    harness.intersect([visibleEntry(replacementTarget)]);    expect(emissions.at(-1)).toEqual(["dynamic"]);     harness.targets.delete("dynamic");    harness.mutate();    expect(harness.unobserve).toHaveBeenCalledWith(replacementTarget);    expect(emissions.at(-1)).toEqual([]);  });   it("rebinds when navigation replaces the settings scroll root", () => {    const harness = createHarness(["section"]);    const emissions: ReadonlyArray<string>[] = [];    observeSettingsSectionVisibility({      container: {} as Element,      targetIds: ["section"],      onChange: (visible) => emissions.push(visible),      environment: harness.environment,    });     const firstTarget = harness.targets.get("section")!;    harness.intersect([visibleEntry(firstTarget)]);    const firstObserverCallback = harness.intersectionCallbacks[0]!;    const nextRoot = { name: "next-root" } as unknown as Element;    const nextTarget = { targetId: "next-section" } as unknown as Element;    harness.targets.set("section", nextTarget);    harness.replaceRoot(nextRoot);     expect(harness.disconnectIntersections).toHaveBeenCalledOnce();    expect(harness.intersectionRoots.at(-1)).toBe(nextRoot);    expect(harness.observed.has(nextTarget)).toBe(true);    expect(emissions.at(-1)).toEqual([]);     firstObserverCallback([visibleEntry(firstTarget)]);    expect(emissions.at(-1)).toEqual([]);    harness.intersect([visibleEntry(nextTarget)]);    expect(emissions.at(-1)).toEqual(["section"]);  });   it("starts observing when the settings scroll root mounts later", () => {    const harness = createHarness(["section"], null);    const emissions: ReadonlyArray<string>[] = [];    observeSettingsSectionVisibility({      container: {} as Element,      targetIds: ["section"],      onChange: (visible) => emissions.push(visible),      environment: harness.environment,    });     expect(emissions).toEqual([[]]);    expect(harness.intersectionRoots).toEqual([]);     const root = { name: "mounted-root" } as unknown as Element;    harness.replaceRoot(root);    harness.intersect([visibleEntry(harness.targets.get("section")!)]);     expect(harness.intersectionRoots).toEqual([root]);    expect(emissions.at(-1)).toEqual(["section"]);  });   it("disconnects both observers and ignores callbacks after cleanup", () => {    const harness = createHarness(["one"]);    const onChange = vi.fn();    const cleanup = observeSettingsSectionVisibility({      container: {} as Element,      targetIds: ["one"],      onChange,      environment: harness.environment,    });     cleanup();    harness.intersect([visibleEntry(harness.targets.get("one")!)]);    harness.mutate();     expect(harness.disconnectIntersections).toHaveBeenCalledOnce();    expect(harness.disconnectMutations).toHaveBeenCalledOnce();    expect(onChange).toHaveBeenCalledTimes(1);    expect(onChange).toHaveBeenLastCalledWith([]);  });});