import type { LegendListRef } from "@legendapp/list/react";import { EnvironmentId, MessageId, ThreadId } from "@t3tools/contracts";import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { observeAssistantCitationCommentSource, observeAssistantCitationSource, type AssistantCitationTarget,} from "./AssistantCitationSource"; const mocks = vi.hoisted(() => ({ resolveRange: vi.fn(), toast: vi.fn() }));vi.mock("~/lib/assistantTextSelection", () => ({ resolveAssistantCitationRange: mocks.resolveRange,}));vi.mock("../ui/toast", () => ({ toastManager: { add: mocks.toast } })); function rect(top: number, height = 20) { return { top, bottom: top + height, height, width: 100 } as DOMRect;} class TestAnimation { currentTime = 0; id = ""; private resolveFinished!: () => void; finished = new Promise<void>((resolve) => { this.resolveFinished = resolve; }); cancel = vi.fn(); finish() { this.resolveFinished(); }} class TestElement { parentNode: TestElement | null = null; dataset: Record<string, string> = {}; focus = vi.fn(); animations: TestAnimation[] = []; animate = vi.fn(() => { const animation = new TestAnimation(); this.animations.push(animation); return animation as unknown as Animation; }); isConnected = true; scrollTop = 8586; scrollHeight = 9334; clientHeight = 748; getBoundingClientRect = () => rect(0, this.clientHeight); ownerDocument = { getSelection: () => null as TestSelection | null, }; contains(node: TestElement | null): boolean { return node === this || (node?.parentNode ? this.contains(node.parentNode) : false); }} class TestRange { startOffset = 0; endOffset = 5; collapsed = false; endContainer: TestElement; constructor( public startContainer: TestElement, readonly readRect: () => DOMRect, ) { this.endContainer = startContainer; } getBoundingClientRect() { return this.collapsed ? rect(0, 0) : this.readRect(); } cloneRange() { const copy = new TestRange(this.startContainer, this.readRect); copy.startOffset = this.startOffset; copy.endOffset = this.endOffset; return copy; } collapseTo(parent: TestElement) { this.startContainer = this.endContainer = parent; this.startOffset = this.endOffset = 0; this.collapsed = true; } setStart(node: TestElement, offset: number) { this.startContainer = node; this.startOffset = offset; this.collapsed = this.startContainer === this.endContainer && this.startOffset === this.endOffset; } setEnd(node: TestElement, offset: number) { this.endContainer = node; this.endOffset = offset; this.collapsed = this.startContainer === this.endContainer && this.startOffset === this.endOffset; } toString() { return this.collapsed ? "" : "quote"; }} class TestSelection { ranges: TestRange[] = []; get rangeCount() { return this.ranges.length; } getRangeAt(index: number) { return this.ranges[index]!; } addRange(range: TestRange) { this.ranges.push(range); } removeRange(range: TestRange) { this.ranges = this.ranges.filter((current) => current !== range); } removeAllRanges() { this.ranges = []; }} function createSource({ reducedMotion = false } = {}) { vi.stubGlobal("window", { matchMedia: vi.fn(() => ({ matches: reducedMotion })) }); let now = 0; vi.stubGlobal("performance", { now: () => now }); const frames = new Map<number, FrameRequestCallback>(); let nextFrame = 0; vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { frames.set(++nextFrame, callback); return nextFrame; }); vi.stubGlobal("cancelAnimationFrame", (frame: number) => frames.delete(frame)); const flushFrame = () => { const callbacks = [...frames.values()]; frames.clear(); for (const callback of callbacks) callback(0); }; const mutationCallbacks = new Set<MutationCallback>(); let resize: ResizeObserverCallback = () => {}; vi.stubGlobal( "MutationObserver", class { constructor(readonly callback: MutationCallback) {} observe() { mutationCallbacks.add(this.callback); } disconnect() { mutationCallbacks.delete(this.callback); } }, ); vi.stubGlobal( "ResizeObserver", class { constructor(callback: ResizeObserverCallback) { resize = callback; } observe() {} disconnect() { resize = () => {}; } }, ); vi.stubGlobal("HTMLElement", TestElement); const highlights = new Map<string, Set<TestRange>>(); vi.stubGlobal( "Highlight", class extends Set<TestRange> { constructor(...ranges: TestRange[]) { super(ranges); } }, ); vi.stubGlobal("CSS", { highlights }); const scrollNode = new TestElement(); const container = new TestElement(); container.parentNode = scrollNode; const root = new TestElement(); root.parentNode = container; const text = new TestElement(); text.parentNode = root; let sourceTop = 200; let measured = true; let rangeHeight = 20; root.getBoundingClientRect = () => rect(sourceTop - scrollNode.scrollTop, 300); mocks.resolveRange.mockImplementation( () => new TestRange(text, () => rect(sourceTop - scrollNode.scrollTop, rangeHeight)), ); const listeners = new Set<() => void>(); const listen = (callback: () => void) => { listeners.add(callback); return () => listeners.delete(callback); }; const layoutChanged = () => { for (const listener of listeners) listener(); }; let pending: { options: { offset: number }; resolve: () => void } | null = null; const scrollToOffset = vi.fn((options: { offset: number; animated?: boolean }) => { pending?.resolve(); return new Promise<void>((resolve) => { pending = { options, resolve }; }); }); const finishScroll = async () => { const scroll = pending; if (!scroll) throw new Error("No citation scroll is pending"); pending = null; scrollNode.scrollTop = scroll.options.offset; scroll.resolve(); await Promise.resolve(); flushFrame(); }; const list = { getScrollableNode: () => scrollNode, getState: () => ({ scroll: scrollNode.scrollTop, indexByKey: () => 1, sizeAtIndex: () => (measured ? 300 : undefined), listenToPosition: (_key: string, callback: () => void) => listen(callback), listen: (_signal: string, callback: () => void) => listen(callback), }), scrollToOffset, } as unknown as LegendListRef; const target: AssistantCitationTarget = { key: "activation-one", citation: { version: 1, environmentId: EnvironmentId.make("environment"), threadId: ThreadId.make("thread"), messageId: MessageId.make("source"), text: "quote", start: 0, end: 5, prefix: "", suffix: "", }, activationRef: { current: { scrolled: false, dismissed: false } }, onComplete: vi.fn(), }; const cleanups: Array<() => void> = []; const mount = (request = target) => { const cleanup = observeAssistantCitationSource({ root: root as unknown as HTMLElement, itemKey: "source-row", request, list, }); if (cleanup) cleanups.push(cleanup); return cleanup; }; const mountComment = ( range = new TestRange(text, () => rect(sourceTop - scrollNode.scrollTop, rangeHeight)), ) => { const onUnavailable = vi.fn(); const cleanup = observeAssistantCitationCommentSource({ anchor: { source: root as unknown as HTMLElement, range: range as unknown as Range, viewport: scrollNode as unknown as HTMLElement, }, citation: target.citation, onUnavailable, }); cleanups.push(cleanup); return { range, onUnavailable, cleanup }; }; const mutation = (changed: TestElement, removed: TestElement[] = []) => { for (const callback of mutationCallbacks) { callback( [{ target: changed, addedNodes: [], removedNodes: removed }] as unknown as MutationRecord[], {} as MutationObserver, ); } }; return { target, root, text, container, scrollNode, highlights, scrollToOffset, mount, mountComment, flushFrame, finishScroll, layoutChanged, mutation, advanceTime: (milliseconds: number) => { now += milliseconds; }, finishPulse: async () => { root.animations.at(-1)?.finish(); await Promise.resolve(); }, resize: () => resize([], {} as ResizeObserver), setMeasured: (value: boolean) => { measured = value; }, setRangeHeight: (value: number) => { rangeHeight = value; }, setSourceTop: (value: number) => { sourceTop = value; }, highlight: () => highlights.get("t3-assistant-citation")?.values().next().value, commentHighlight: () => highlights.get("t3-assistant-citation-comment"), cleanup: () => { for (const cleanup of cleanups) cleanup(); }, };} afterEach(() => { vi.unstubAllGlobals(); vi.clearAllMocks();}); describe("assistant citation source lifecycle", () => { it("waits for a mounted measurement and rechecks geometry after the list settles", async () => { const source = createSource(); source.setMeasured(false); source.mount(); source.flushFrame(); expect(source.scrollToOffset).not.toHaveBeenCalled(); source.setMeasured(true); source.setRangeHeight(0); source.resize(); source.flushFrame(); expect(source.target.activationRef.current.scrolled).toBe(false); expect(source.scrollToOffset).not.toHaveBeenCalled(); source.setRangeHeight(20); source.layoutChanged(); source.flushFrame(); expect(source.scrollToOffset).toHaveBeenCalledExactlyOnceWith({ offset: 80, animated: true }); expect(source.highlight()).toBeUndefined(); expect(source.root.animations).toHaveLength(0); source.scrollNode.scrollTop = 4200; source.advanceTime(700); source.setSourceTop(260); source.layoutChanged(); source.flushFrame(); expect(source.scrollToOffset).toHaveBeenCalledTimes(1); expect(source.highlight()).toBeUndefined(); expect(source.target.onComplete).not.toHaveBeenCalled(); await source.finishScroll(); expect(source.target.onComplete).not.toHaveBeenCalled(); expect(source.scrollToOffset).toHaveBeenLastCalledWith({ offset: 140, animated: true }); await source.finishScroll(); expect(source.target.onComplete).toHaveBeenCalledOnce(); expect(source.highlight()?.getBoundingClientRect().top).toBe(120); expect(source.root.animations).toHaveLength(1); expect(source.root.focus).not.toHaveBeenCalled(); source.cleanup(); expect(source.highlight()).toBeUndefined(); }); it("completes a measured citation at the clamped beginning of the list", async () => { const source = createSource(); source.setSourceTop(40); source.mount(); source.flushFrame(); await source.finishScroll(); expect(source.scrollNode.scrollTop).toBe(0); expect(source.highlight()?.toString()).toBe("quote"); expect(source.target.onComplete).toHaveBeenCalledOnce(); source.cleanup(); }); it("repairs collapsed ranges after an ancestor move without following the user's scroll", async () => { const source = createSource(); const unmount = source.mount(); source.flushFrame(); await source.finishScroll(); const originalRange = source.highlight()!; source.scrollNode.scrollTop = 4000; originalRange.collapseTo(source.scrollNode); source.mutation(source.scrollNode, [source.container]); expect(source.highlight()).toBeUndefined(); expect(source.root.dataset.citationHighlighted).toBeUndefined(); source.flushFrame(); expect(source.highlight()?.toString()).toBe("quote"); expect(source.root.contains(source.highlight()!.startContainer)).toBe(true); expect(source.scrollNode.scrollTop).toBe(4000); expect(source.root.animations).toHaveLength(1); const resolutions = mocks.resolveRange.mock.calls.length; source.mutation(new TestElement()); source.flushFrame(); expect(mocks.resolveRange).toHaveBeenCalledTimes(resolutions); unmount?.(); source.advanceTime(700); source.mount(); source.flushFrame(); expect(source.highlight()?.toString()).toBe("quote"); expect(source.scrollToOffset).toHaveBeenCalledTimes(1); expect(source.target.onComplete).toHaveBeenCalledOnce(); expect(source.root.animations).toHaveLength(2); expect(source.root.animations[1]?.currentTime).toBe(700); source.root.animations[0]?.finish(); await Promise.resolve(); expect(source.highlight()?.toString()).toBe("quote"); await source.finishPulse(); expect(source.highlight()).toBeUndefined(); source.cleanup(); }); it("removes the quote after the pulse and replays it only for a fresh citation activation", async () => { const source = createSource(); const unmount = source.mount(); source.flushFrame(); await source.finishScroll(); await source.finishPulse(); expect(source.highlight()).toBeUndefined(); expect(source.root.dataset.citationHighlighted).toBeUndefined(); source.layoutChanged(); source.flushFrame(); expect(source.mount()).toBeUndefined(); expect(source.root.animations).toHaveLength(1); unmount?.(); source.mount({ ...source.target, key: "activation-two", activationRef: { current: { scrolled: false, dismissed: false } }, }); source.flushFrame(); expect(source.highlight()?.toString()).toBe("quote"); expect(source.root.animations).toHaveLength(2); expect(source.root.animations[1]?.currentTime).toBe(0); source.cleanup(); }); it("uses instant navigation and removes the temporary quote highlight for reduced motion", async () => { const source = createSource({ reducedMotion: true }); source.mount(); source.flushFrame(); expect(source.scrollToOffset).toHaveBeenCalledWith({ offset: 80, animated: false }); expect(source.highlight()).toBeUndefined(); await source.finishScroll(); expect(source.target.activationRef.current.pulse?.reducedMotion).toBe(true); expect(source.highlight()?.toString()).toBe("quote"); await source.finishPulse(); expect(source.highlight()).toBeUndefined(); expect(source.root.focus).not.toHaveBeenCalled(); source.cleanup(); }); it("does not revive a quote when remounting after its original pulse deadline", async () => { const source = createSource(); const unmount = source.mount(); source.flushFrame(); await source.finishScroll(); unmount?.(); source.advanceTime(3100); source.mount(); source.flushFrame(); expect(source.highlight()).toBeUndefined(); expect(source.root.animations).toHaveLength(1); expect(source.target.activationRef.current.dismissed).toBe(true); source.cleanup(); }); it("dismisses an active text pulse without reviving it on remount", async () => { const source = createSource(); source.mount(); source.flushFrame(); await source.finishScroll(); source.target.activationRef.current.dismissed = true; source.cleanup(); expect(source.root.animations[0]?.cancel).toHaveBeenCalledOnce(); expect(source.highlight()).toBeUndefined(); expect(source.mount()).toBeUndefined(); source.flushFrame(); expect(source.root.animations).toHaveLength(1); }); it("does not highlight a parent when the saved quote no longer matches", async () => { const source = createSource(); mocks.resolveRange.mockReturnValue(null); source.mount(); source.flushFrame(); await source.finishScroll(); expect(source.highlight()).toBeUndefined(); expect(source.root.animations).toHaveLength(0); expect(source.root.focus).not.toHaveBeenCalled(); expect(source.target.onComplete).toHaveBeenCalledOnce(); source.cleanup(); }); it("cancels deferred positioning without capturing a pre-gesture offset or reviving dismissal", async () => {Lines 1–500 of 744. Load more
import type { LegendListRef } from "@legendapp/list/react"; import { EnvironmentId, MessageId, ThreadId } from "@t3tools/contracts"; import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { observeAssistantCitationCommentSource, observeAssistantCitationSource, type AssistantCitationTarget, } from "./AssistantCitationSource"; const mocks = vi.hoisted(() => ({ resolveRange: vi.fn(), toast: vi.fn() })); vi.mock("~/lib/assistantTextSelection", () => ({ resolveAssistantCitationRange: mocks.resolveRange, })); vi.mock("../ui/toast", () => ({ toastManager: { add: mocks.toast } })); function rect(top: number, height = 20) { return { top, bottom: top + height, height, width: 100 } as DOMRect; } class TestAnimation { currentTime = 0; id = ""; private resolveFinished!: () => void; finished = new Promise<void>((resolve) => { this.resolveFinished = resolve; }); cancel = vi.fn(); finish() { this.resolveFinished(); } } class TestElement { parentNode: TestElement | null = null; dataset: Record<string, string> = {}; focus = vi.fn(); animations: TestAnimation[] = []; animate = vi.fn(() => { const animation = new TestAnimation(); this.animations.push(animation); return animation as unknown as Animation; }); isConnected = true; scrollTop = 8586; scrollHeight = 9334; clientHeight = 748; getBoundingClientRect = () => rect(0, this.clientHeight); ownerDocument = { getSelection: () => null as TestSelection | null, }; contains(node: TestElement | null): boolean { return node === this || (node?.parentNode ? this.contains(node.parentNode) : false); } } class TestRange { startOffset = 0; endOffset = 5; collapsed = false; endContainer: TestElement; constructor( public startContainer: TestElement, readonly readRect: () => DOMRect, ) { this.endContainer = startContainer; } getBoundingClientRect() { return this.collapsed ? rect(0, 0) : this.readRect(); } cloneRange() { const copy = new TestRange(this.startContainer, this.readRect); copy.startOffset = this.startOffset; copy.endOffset = this.endOffset; return copy; } collapseTo(parent: TestElement) { this.startContainer = this.endContainer = parent; this.startOffset = this.endOffset = 0; this.collapsed = true; } setStart(node: TestElement, offset: number) { this.startContainer = node; this.startOffset = offset; this.collapsed = this.startContainer === this.endContainer && this.startOffset === this.endOffset; } setEnd(node: TestElement, offset: number) { this.endContainer = node; this.endOffset = offset; this.collapsed = this.startContainer === this.endContainer && this.startOffset === this.endOffset; } toString() { return this.collapsed ? "" : "quote"; } } class TestSelection { ranges: TestRange[] = []; get rangeCount() { return this.ranges.length; } getRangeAt(index: number) { return this.ranges[index]!; } addRange(range: TestRange) { this.ranges.push(range); } removeRange(range: TestRange) { this.ranges = this.ranges.filter((current) => current !== range); } removeAllRanges() { this.ranges = []; } } function createSource({ reducedMotion = false } = {}) { vi.stubGlobal("window", { matchMedia: vi.fn(() => ({ matches: reducedMotion })) }); let now = 0; vi.stubGlobal("performance", { now: () => now }); const frames = new Map<number, FrameRequestCallback>(); let nextFrame = 0; vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { frames.set(++nextFrame, callback); return nextFrame; }); vi.stubGlobal("cancelAnimationFrame", (frame: number) => frames.delete(frame)); const flushFrame = () => { const callbacks = [...frames.values()]; frames.clear(); for (const callback of callbacks) callback(0); }; const mutationCallbacks = new Set<MutationCallback>(); let resize: ResizeObserverCallback = () => {}; vi.stubGlobal( "MutationObserver", class { constructor(readonly callback: MutationCallback) {} observe() { mutationCallbacks.add(this.callback); } disconnect() { mutationCallbacks.delete(this.callback); } }, ); vi.stubGlobal( "ResizeObserver", class { constructor(callback: ResizeObserverCallback) { resize = callback; } observe() {} disconnect() { resize = () => {}; } }, ); vi.stubGlobal("HTMLElement", TestElement); const highlights = new Map<string, Set<TestRange>>(); vi.stubGlobal( "Highlight", class extends Set<TestRange> { constructor(...ranges: TestRange[]) { super(ranges); } }, ); vi.stubGlobal("CSS", { highlights }); const scrollNode = new TestElement(); const container = new TestElement(); container.parentNode = scrollNode; const root = new TestElement(); root.parentNode = container; const text = new TestElement(); text.parentNode = root; let sourceTop = 200; let measured = true; let rangeHeight = 20; root.getBoundingClientRect = () => rect(sourceTop - scrollNode.scrollTop, 300); mocks.resolveRange.mockImplementation( () => new TestRange(text, () => rect(sourceTop - scrollNode.scrollTop, rangeHeight)), ); const listeners = new Set<() => void>(); const listen = (callback: () => void) => { listeners.add(callback); return () => listeners.delete(callback); }; const layoutChanged = () => { for (const listener of listeners) listener(); }; let pending: { options: { offset: number }; resolve: () => void } | null = null; const scrollToOffset = vi.fn((options: { offset: number; animated?: boolean }) => { pending?.resolve(); return new Promise<void>((resolve) => { pending = { options, resolve }; }); }); const finishScroll = async () => { const scroll = pending; if (!scroll) throw new Error("No citation scroll is pending"); pending = null; scrollNode.scrollTop = scroll.options.offset; scroll.resolve(); await Promise.resolve(); flushFrame(); }; const list = { getScrollableNode: () => scrollNode, getState: () => ({ scroll: scrollNode.scrollTop, indexByKey: () => 1, sizeAtIndex: () => (measured ? 300 : undefined), listenToPosition: (_key: string, callback: () => void) => listen(callback), listen: (_signal: string, callback: () => void) => listen(callback), }), scrollToOffset, } as unknown as LegendListRef; const target: AssistantCitationTarget = { key: "activation-one", citation: { version: 1, environmentId: EnvironmentId.make("environment"), threadId: ThreadId.make("thread"), messageId: MessageId.make("source"), text: "quote", start: 0, end: 5, prefix: "", suffix: "", }, activationRef: { current: { scrolled: false, dismissed: false } }, onComplete: vi.fn(), }; const cleanups: Array<() => void> = []; const mount = (request = target) => { const cleanup = observeAssistantCitationSource({ root: root as unknown as HTMLElement, itemKey: "source-row", request, list, }); if (cleanup) cleanups.push(cleanup); return cleanup; }; const mountComment = ( range = new TestRange(text, () => rect(sourceTop - scrollNode.scrollTop, rangeHeight)), ) => { const onUnavailable = vi.fn(); const cleanup = observeAssistantCitationCommentSource({ anchor: { source: root as unknown as HTMLElement, range: range as unknown as Range, viewport: scrollNode as unknown as HTMLElement, }, citation: target.citation, onUnavailable, }); cleanups.push(cleanup); return { range, onUnavailable, cleanup }; }; const mutation = (changed: TestElement, removed: TestElement[] = []) => { for (const callback of mutationCallbacks) { callback( [{ target: changed, addedNodes: [], removedNodes: removed }] as unknown as MutationRecord[], {} as MutationObserver, ); } }; return { target, root, text, container, scrollNode, highlights, scrollToOffset, mount, mountComment, flushFrame, finishScroll, layoutChanged, mutation, advanceTime: (milliseconds: number) => { now += milliseconds; }, finishPulse: async () => { root.animations.at(-1)?.finish(); await Promise.resolve(); }, resize: () => resize([], {} as ResizeObserver), setMeasured: (value: boolean) => { measured = value; }, setRangeHeight: (value: number) => { rangeHeight = value; }, setSourceTop: (value: number) => { sourceTop = value; }, highlight: () => highlights.get("t3-assistant-citation")?.values().next().value, commentHighlight: () => highlights.get("t3-assistant-citation-comment"), cleanup: () => { for (const cleanup of cleanups) cleanup(); }, }; } afterEach(() => { vi.unstubAllGlobals(); vi.clearAllMocks(); }); describe("assistant citation source lifecycle", () => { it("waits for a mounted measurement and rechecks geometry after the list settles", async () => { const source = createSource(); source.setMeasured(false); source.mount(); source.flushFrame(); expect(source.scrollToOffset).not.toHaveBeenCalled(); source.setMeasured(true); source.setRangeHeight(0); source.resize(); source.flushFrame(); expect(source.target.activationRef.current.scrolled).toBe(false); expect(source.scrollToOffset).not.toHaveBeenCalled(); source.setRangeHeight(20); source.layoutChanged(); source.flushFrame(); expect(source.scrollToOffset).toHaveBeenCalledExactlyOnceWith({ offset: 80, animated: true }); expect(source.highlight()).toBeUndefined(); expect(source.root.animations).toHaveLength(0); source.scrollNode.scrollTop = 4200; source.advanceTime(700); source.setSourceTop(260); source.layoutChanged(); source.flushFrame(); expect(source.scrollToOffset).toHaveBeenCalledTimes(1); expect(source.highlight()).toBeUndefined(); expect(source.target.onComplete).not.toHaveBeenCalled(); await source.finishScroll(); expect(source.target.onComplete).not.toHaveBeenCalled(); expect(source.scrollToOffset).toHaveBeenLastCalledWith({ offset: 140, animated: true }); await source.finishScroll(); expect(source.target.onComplete).toHaveBeenCalledOnce(); expect(source.highlight()?.getBoundingClientRect().top).toBe(120); expect(source.root.animations).toHaveLength(1); expect(source.root.focus).not.toHaveBeenCalled(); source.cleanup(); expect(source.highlight()).toBeUndefined(); }); it("completes a measured citation at the clamped beginning of the list", async () => { const source = createSource(); source.setSourceTop(40); source.mount(); source.flushFrame(); await source.finishScroll(); expect(source.scrollNode.scrollTop).toBe(0); expect(source.highlight()?.toString()).toBe("quote"); expect(source.target.onComplete).toHaveBeenCalledOnce(); source.cleanup(); }); it("repairs collapsed ranges after an ancestor move without following the user's scroll", async () => { const source = createSource(); const unmount = source.mount(); source.flushFrame(); await source.finishScroll(); const originalRange = source.highlight()!; source.scrollNode.scrollTop = 4000; originalRange.collapseTo(source.scrollNode); source.mutation(source.scrollNode, [source.container]); expect(source.highlight()).toBeUndefined(); expect(source.root.dataset.citationHighlighted).toBeUndefined(); source.flushFrame(); expect(source.highlight()?.toString()).toBe("quote"); expect(source.root.contains(source.highlight()!.startContainer)).toBe(true); expect(source.scrollNode.scrollTop).toBe(4000); expect(source.root.animations).toHaveLength(1); const resolutions = mocks.resolveRange.mock.calls.length; source.mutation(new TestElement()); source.flushFrame(); expect(mocks.resolveRange).toHaveBeenCalledTimes(resolutions); unmount?.(); source.advanceTime(700); source.mount(); source.flushFrame(); expect(source.highlight()?.toString()).toBe("quote"); expect(source.scrollToOffset).toHaveBeenCalledTimes(1); expect(source.target.onComplete).toHaveBeenCalledOnce(); expect(source.root.animations).toHaveLength(2); expect(source.root.animations[1]?.currentTime).toBe(700); source.root.animations[0]?.finish(); await Promise.resolve(); expect(source.highlight()?.toString()).toBe("quote"); await source.finishPulse(); expect(source.highlight()).toBeUndefined(); source.cleanup(); }); it("removes the quote after the pulse and replays it only for a fresh citation activation", async () => { const source = createSource(); const unmount = source.mount(); source.flushFrame(); await source.finishScroll(); await source.finishPulse(); expect(source.highlight()).toBeUndefined(); expect(source.root.dataset.citationHighlighted).toBeUndefined(); source.layoutChanged(); source.flushFrame(); expect(source.mount()).toBeUndefined(); expect(source.root.animations).toHaveLength(1); unmount?.(); source.mount({ ...source.target, key: "activation-two", activationRef: { current: { scrolled: false, dismissed: false } }, }); source.flushFrame(); expect(source.highlight()?.toString()).toBe("quote"); expect(source.root.animations).toHaveLength(2); expect(source.root.animations[1]?.currentTime).toBe(0); source.cleanup(); }); it("uses instant navigation and removes the temporary quote highlight for reduced motion", async () => { const source = createSource({ reducedMotion: true }); source.mount(); source.flushFrame(); expect(source.scrollToOffset).toHaveBeenCalledWith({ offset: 80, animated: false }); expect(source.highlight()).toBeUndefined(); await source.finishScroll(); expect(source.target.activationRef.current.pulse?.reducedMotion).toBe(true); expect(source.highlight()?.toString()).toBe("quote"); await source.finishPulse(); expect(source.highlight()).toBeUndefined(); expect(source.root.focus).not.toHaveBeenCalled(); source.cleanup(); }); it("does not revive a quote when remounting after its original pulse deadline", async () => { const source = createSource(); const unmount = source.mount(); source.flushFrame(); await source.finishScroll(); unmount?.(); source.advanceTime(3100); source.mount(); source.flushFrame(); expect(source.highlight()).toBeUndefined(); expect(source.root.animations).toHaveLength(1); expect(source.target.activationRef.current.dismissed).toBe(true); source.cleanup(); }); it("dismisses an active text pulse without reviving it on remount", async () => { const source = createSource(); source.mount(); source.flushFrame(); await source.finishScroll(); source.target.activationRef.current.dismissed = true; source.cleanup(); expect(source.root.animations[0]?.cancel).toHaveBeenCalledOnce(); expect(source.highlight()).toBeUndefined(); expect(source.mount()).toBeUndefined(); source.flushFrame(); expect(source.root.animations).toHaveLength(1); }); it("does not highlight a parent when the saved quote no longer matches", async () => { const source = createSource(); mocks.resolveRange.mockReturnValue(null); source.mount(); source.flushFrame(); await source.finishScroll(); expect(source.highlight()).toBeUndefined(); expect(source.root.animations).toHaveLength(0); expect(source.root.focus).not.toHaveBeenCalled(); expect(source.target.onComplete).toHaveBeenCalledOnce(); source.cleanup(); }); it("cancels deferred positioning without capturing a pre-gesture offset or reviving dismissal", async () => {