2026-04-17 12:14:55 +03:00
|
|
|
/**
|
|
|
|
|
* @vitest-environment jsdom
|
|
|
|
|
*/
|
2026-03-02 22:18:41 +03:00
|
|
|
import {
|
|
|
|
|
afterEach,
|
|
|
|
|
beforeEach,
|
|
|
|
|
describe,
|
|
|
|
|
expect,
|
|
|
|
|
it,
|
|
|
|
|
vi,
|
|
|
|
|
} from 'vitest';
|
|
|
|
|
import {
|
|
|
|
|
type BreadcrumbItem,
|
|
|
|
|
createScrollBreadcrumbsStore,
|
|
|
|
|
} from './scrollBreadcrumbsStore.svelte';
|
|
|
|
|
|
|
|
|
|
// Mock IntersectionObserver - class variable to track instances
|
|
|
|
|
let mockObserverInstances: MockIntersectionObserver[] = [];
|
|
|
|
|
|
|
|
|
|
class MockIntersectionObserver implements IntersectionObserver {
|
|
|
|
|
root = null;
|
|
|
|
|
rootMargin = '';
|
|
|
|
|
thresholds: number[] = [];
|
|
|
|
|
readonly callbacks: Array<(entries: IntersectionObserverEntry[], observer: IntersectionObserver) => void> = [];
|
|
|
|
|
readonly observedElements = new Set<Element>();
|
|
|
|
|
|
|
|
|
|
constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
|
|
|
|
|
this.callbacks.push(callback);
|
|
|
|
|
if (options?.rootMargin) this.rootMargin = options.rootMargin;
|
|
|
|
|
if (options?.threshold) {
|
|
|
|
|
this.thresholds = Array.isArray(options.threshold) ? options.threshold : [options.threshold];
|
|
|
|
|
}
|
|
|
|
|
mockObserverInstances.push(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
observe(target: Element): void {
|
|
|
|
|
this.observedElements.add(target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unobserve(target: Element): void {
|
|
|
|
|
this.observedElements.delete(target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnect(): void {
|
|
|
|
|
this.observedElements.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
takeRecords(): IntersectionObserverEntry[] {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper method for tests to trigger intersection changes
|
|
|
|
|
triggerIntersection(target: Element, isIntersecting: boolean): void {
|
|
|
|
|
const entry: Partial<IntersectionObserverEntry> = {
|
|
|
|
|
target,
|
|
|
|
|
isIntersecting,
|
|
|
|
|
intersectionRatio: isIntersecting ? 1 : 0,
|
|
|
|
|
boundingClientRect: {} as DOMRectReadOnly,
|
|
|
|
|
intersectionRect: {} as DOMRectReadOnly,
|
|
|
|
|
rootBounds: null,
|
|
|
|
|
time: Date.now(),
|
|
|
|
|
};
|
|
|
|
|
this.callbacks.forEach(cb => cb([entry as IntersectionObserverEntry], this));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('ScrollBreadcrumbsStore', () => {
|
|
|
|
|
let scrollListeners: Array<() => void> = [];
|
|
|
|
|
let addEventListenerSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
|
let removeEventListenerSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
|
let scrollToSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
|
|
|
|
|
|
// Helper to create mock elements
|
|
|
|
|
const createMockElement = (): HTMLElement => {
|
|
|
|
|
const el = document.createElement('div');
|
|
|
|
|
Object.defineProperty(el, 'getBoundingClientRect', {
|
|
|
|
|
value: vi.fn(() => ({
|
|
|
|
|
top: 100,
|
|
|
|
|
left: 0,
|
|
|
|
|
bottom: 200,
|
|
|
|
|
right: 100,
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 100,
|
|
|
|
|
toJSON: () => ({}),
|
|
|
|
|
})),
|
|
|
|
|
});
|
|
|
|
|
return el;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helper to create breadcrumb item
|
|
|
|
|
const createItem = (index: number, title: string, element?: HTMLElement): BreadcrumbItem => ({
|
|
|
|
|
index,
|
|
|
|
|
title,
|
|
|
|
|
element: element ?? createMockElement(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
mockObserverInstances = [];
|
|
|
|
|
scrollListeners = [];
|
|
|
|
|
|
|
|
|
|
// Set up IntersectionObserver mock before creating store
|
|
|
|
|
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
|
|
|
|
|
|
|
|
|
|
// Mock window.scrollTo
|
|
|
|
|
scrollToSpy = vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
|
|
|
|
|
|
|
|
|
|
// Track scroll event listeners
|
|
|
|
|
addEventListenerSpy = vi.spyOn(window, 'addEventListener').mockImplementation(
|
|
|
|
|
(event: string, listener: EventListenerOrEventListenerObject, options?: any) => {
|
|
|
|
|
if (event === 'scroll') {
|
|
|
|
|
scrollListeners.push(listener as () => void);
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
removeEventListenerSpy = vi.spyOn(window, 'removeEventListener').mockImplementation(
|
|
|
|
|
(event: string, listener: EventListenerOrEventListenerObject) => {
|
|
|
|
|
if (event === 'scroll') {
|
|
|
|
|
const index = scrollListeners.indexOf(listener as () => void);
|
|
|
|
|
if (index > -1) scrollListeners.splice(index, 1);
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.restoreAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Adding items', () => {
|
|
|
|
|
it('should add an item and track it', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
const item = createItem(0, 'Section 1', element);
|
|
|
|
|
|
|
|
|
|
store.add(item);
|
|
|
|
|
|
|
|
|
|
expect(store.items).toHaveLength(1);
|
|
|
|
|
expect(store.items[0]).toEqual(item);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should ignore duplicate indices', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element1 = createMockElement();
|
|
|
|
|
const element2 = createMockElement();
|
|
|
|
|
|
|
|
|
|
store.add(createItem(0, 'First', element1));
|
|
|
|
|
store.add(createItem(0, 'Second', element2));
|
|
|
|
|
|
|
|
|
|
expect(store.items).toHaveLength(1);
|
|
|
|
|
expect(store.items[0].title).toBe('First');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should add multiple items with different indices', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
store.add(createItem(2, 'Third'));
|
|
|
|
|
|
|
|
|
|
expect(store.items).toHaveLength(3);
|
|
|
|
|
expect(store.items.map(i => i.index)).toEqual([0, 1, 2]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should attach scroll listener when first item is added', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
expect(scrollListeners).toHaveLength(0);
|
|
|
|
|
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
|
|
|
|
|
expect(scrollListeners).toHaveLength(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should initialize observer with element', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'Test', element));
|
|
|
|
|
|
|
|
|
|
expect(mockObserverInstances).toHaveLength(1);
|
|
|
|
|
expect(mockObserverInstances[0].observedElements.has(element)).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Removing items', () => {
|
|
|
|
|
it('should remove an item by index', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
store.add(createItem(2, 'Third'));
|
|
|
|
|
|
|
|
|
|
store.remove(1);
|
|
|
|
|
|
|
|
|
|
expect(store.items).toHaveLength(2);
|
|
|
|
|
expect(store.items.map(i => i.index)).toEqual([0, 2]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should do nothing when removing non-existent index', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
|
|
|
|
|
store.remove(999);
|
|
|
|
|
|
|
|
|
|
expect(store.items).toHaveLength(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should unobserve element when removed', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'First', element));
|
|
|
|
|
|
|
|
|
|
expect(mockObserverInstances[0].observedElements.has(element)).toBe(true);
|
|
|
|
|
|
|
|
|
|
store.remove(0);
|
|
|
|
|
|
|
|
|
|
expect(mockObserverInstances[0].observedElements.has(element)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should disconnect observer when no items remain', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
|
|
|
|
|
expect(addEventListenerSpy).toHaveBeenCalled();
|
|
|
|
|
const initialCallCount = addEventListenerSpy.mock.calls.length;
|
|
|
|
|
|
|
|
|
|
store.remove(0);
|
|
|
|
|
// addEventListener was called for the first item, still 1 call
|
|
|
|
|
expect(addEventListenerSpy.mock.calls.length).toBe(initialCallCount);
|
|
|
|
|
|
|
|
|
|
store.remove(1);
|
|
|
|
|
// The listener count should be 0 now - disconnect was called
|
|
|
|
|
// We verify the observer was disconnected
|
|
|
|
|
expect(mockObserverInstances[0].observedElements.size).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should reattach listener when adding after cleanup', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
store.remove(0);
|
|
|
|
|
|
|
|
|
|
expect(scrollListeners).toHaveLength(0);
|
|
|
|
|
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
expect(scrollListeners).toHaveLength(1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Intersection Observer behavior', () => {
|
|
|
|
|
it('should add to scrolledPast when element exits viewport while scrolling down', () => {
|
|
|
|
|
// Set initial scrollY before creating store/adding items
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 0, writable: true, configurable: true });
|
|
|
|
|
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'First', element));
|
|
|
|
|
|
|
|
|
|
// Simulate scrolling down (scrollY increases)
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 100, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
|
|
|
|
|
// Trigger intersection: element exits viewport while scrolling down
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element, false);
|
|
|
|
|
|
|
|
|
|
expect(store.isScrolledPast(0)).toBe(true);
|
|
|
|
|
expect(store.scrolledPastItems).toHaveLength(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not add to scrolledPast when not scrolling down', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'First', element));
|
|
|
|
|
|
|
|
|
|
// scrollY stays at 0 (not scrolling down)
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 0, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
|
|
|
|
|
// Element exits viewport
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element, false);
|
|
|
|
|
|
|
|
|
|
expect(store.isScrolledPast(0)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should remove from scrolledPast when element enters viewport while scrolling up', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'First', element));
|
|
|
|
|
|
|
|
|
|
// First, scroll down and exit viewport
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 100, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element, false);
|
|
|
|
|
|
|
|
|
|
expect(store.isScrolledPast(0)).toBe(true);
|
|
|
|
|
|
|
|
|
|
// Now scroll up (decrease scrollY) and element enters viewport
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 50, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element, true);
|
|
|
|
|
|
|
|
|
|
expect(store.isScrolledPast(0)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('scrollTo method', () => {
|
|
|
|
|
it('should scroll to item by index with window as container', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'First', element));
|
|
|
|
|
|
|
|
|
|
store.scrollTo(0, window);
|
|
|
|
|
|
|
|
|
|
expect(scrollToSpy).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should do nothing when index does not exist', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
|
|
|
|
|
store.scrollTo(999);
|
|
|
|
|
|
|
|
|
|
expect(scrollToSpy).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use scroll offset when calculating target position', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
|
|
|
|
|
// Reset the mock to clear previous calls
|
|
|
|
|
scrollToSpy.mockClear();
|
|
|
|
|
|
|
|
|
|
// Create fresh mock element with specific getBoundingClientRect
|
|
|
|
|
const element = document.createElement('div');
|
|
|
|
|
const getBoundingClientRectMock = vi.fn(() => ({
|
|
|
|
|
top: 200,
|
|
|
|
|
left: 0,
|
|
|
|
|
bottom: 300,
|
|
|
|
|
right: 100,
|
|
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 200,
|
|
|
|
|
toJSON: () => ({}),
|
|
|
|
|
}));
|
|
|
|
|
Object.defineProperty(element, 'getBoundingClientRect', {
|
|
|
|
|
value: getBoundingClientRectMock,
|
|
|
|
|
writable: true,
|
|
|
|
|
configurable: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add item with 80px offset
|
|
|
|
|
store.add(createItem(0, 'Third', element), 80);
|
|
|
|
|
|
|
|
|
|
store.scrollTo(0);
|
|
|
|
|
|
|
|
|
|
// The offset should be subtracted from the element position
|
|
|
|
|
// 200 - 80 = 120 (but in jsdom, getBoundingClientRect might have different behavior)
|
|
|
|
|
// Let's just verify smooth behavior is used
|
|
|
|
|
expect(scrollToSpy).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Verify that the scroll position is less than the element top (offset was applied)
|
|
|
|
|
const scrollToCall = scrollToSpy.mock.calls[0][0] as ScrollToOptions;
|
|
|
|
|
expect((scrollToCall as any).top).toBeLessThan(200);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle HTMLElement container', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'Test', element));
|
|
|
|
|
|
|
|
|
|
const container: HTMLElement = {
|
|
|
|
|
scrollTop: 50,
|
|
|
|
|
scrollTo: vi.fn(),
|
|
|
|
|
getBoundingClientRect: () => ({
|
|
|
|
|
top: 0,
|
|
|
|
|
bottom: 500,
|
|
|
|
|
left: 0,
|
|
|
|
|
right: 400,
|
|
|
|
|
width: 400,
|
|
|
|
|
height: 500,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
toJSON: () => ({}),
|
|
|
|
|
}),
|
|
|
|
|
} as any;
|
|
|
|
|
|
|
|
|
|
store.scrollTo(0, container);
|
|
|
|
|
|
|
|
|
|
expect(container.scrollTo).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
behavior: 'smooth',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Getters', () => {
|
|
|
|
|
it('should return items sorted by index', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
store.add(createItem(2, 'Third'));
|
|
|
|
|
|
|
|
|
|
expect(store.items.map(i => i.index)).toEqual([0, 1, 2]);
|
|
|
|
|
expect(store.items.map(i => i.title)).toEqual(['First', 'Second', 'Third']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return empty scrolledPastItems initially', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
|
|
|
|
|
expect(store.scrolledPastItems).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return items that have been scrolled past', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'First', element));
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
|
|
|
|
|
// Simulate scrolling down and element exiting viewport
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 100, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element, false);
|
|
|
|
|
|
|
|
|
|
expect(store.scrolledPastItems).toHaveLength(1);
|
|
|
|
|
expect(store.scrolledPastItems[0].index).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('activeIndex getter', () => {
|
|
|
|
|
it('should return null when no items are scrolled past', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
|
|
|
|
|
expect(store.activeIndex).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return the last scrolled item index', () => {
|
|
|
|
|
// Set initial scroll position
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 0, writable: true, configurable: true });
|
|
|
|
|
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element0 = createMockElement();
|
|
|
|
|
const element1 = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'First', element0));
|
|
|
|
|
store.add(createItem(1, 'Second', element1));
|
|
|
|
|
|
|
|
|
|
// Scroll down, first item exits
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 100, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element0, false);
|
|
|
|
|
|
|
|
|
|
expect(store.activeIndex).toBe(0);
|
|
|
|
|
|
|
|
|
|
// Second item exits
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element1, false);
|
|
|
|
|
|
|
|
|
|
expect(store.activeIndex).toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should update active index when scrolling back up', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element0 = createMockElement();
|
|
|
|
|
const element1 = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'First', element0));
|
|
|
|
|
store.add(createItem(1, 'Second', element1));
|
|
|
|
|
|
|
|
|
|
// Scroll past both items
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 200, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element0, false);
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element1, false);
|
|
|
|
|
|
|
|
|
|
expect(store.activeIndex).toBe(1);
|
|
|
|
|
|
|
|
|
|
// Scroll back up, item 1 enters viewport
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 100, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element1, true);
|
|
|
|
|
|
|
|
|
|
expect(store.activeIndex).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('isScrolledPast', () => {
|
|
|
|
|
it('should return false for items not scrolled past', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
|
|
|
|
|
expect(store.isScrolledPast(0)).toBe(false);
|
|
|
|
|
expect(store.isScrolledPast(1)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return true for scrolled items', () => {
|
|
|
|
|
// Set initial scroll position
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 0, writable: true, configurable: true });
|
|
|
|
|
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'First', element));
|
|
|
|
|
store.add(createItem(1, 'Second'));
|
|
|
|
|
|
|
|
|
|
// Scroll down, first item exits viewport
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 100, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element, false);
|
|
|
|
|
|
|
|
|
|
expect(store.isScrolledPast(0)).toBe(true);
|
|
|
|
|
expect(store.isScrolledPast(1)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return false for non-existent indices', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
store.add(createItem(0, 'First'));
|
|
|
|
|
|
|
|
|
|
expect(store.isScrolledPast(999)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Scroll direction tracking', () => {
|
|
|
|
|
it('should track scroll direction changes', () => {
|
|
|
|
|
const store = createScrollBreadcrumbsStore();
|
|
|
|
|
const element = createMockElement();
|
|
|
|
|
store.add(createItem(0, 'First', element));
|
|
|
|
|
|
|
|
|
|
// Initial scroll position
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 0, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
|
|
|
|
|
// Scroll down
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 100, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element, false);
|
|
|
|
|
|
|
|
|
|
// Should be in scrolledPast since we scrolled down
|
|
|
|
|
expect(store.isScrolledPast(0)).toBe(true);
|
|
|
|
|
|
|
|
|
|
// Scroll back up
|
|
|
|
|
Object.defineProperty(window, 'scrollY', { value: 50, writable: true, configurable: true });
|
|
|
|
|
scrollListeners.forEach(l => l());
|
|
|
|
|
mockObserverInstances[0].triggerIntersection(element, true);
|
|
|
|
|
|
|
|
|
|
// Should be removed since we scrolled up
|
|
|
|
|
expect(store.isScrolledPast(0)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|