import { TestBed, ComponentFixture } from "@angular/core/testing"; import { Component, ViewChild } from "@angular/core"; import { By } from "@angular/platform-browser"; // Load the implementations that should be tested import { SamStickyComponent, OffsetParentLike } from "./sticky.component"; @Component({ selector: "test-cmp", template: `
content
`, standalone: false, }) class TestComponent { @ViewChild("var", { static: true }) var; } describe("The Sam Sticky directive", () => { let directive: SamStickyComponent; let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ declarations: [SamStickyComponent, TestComponent], }); fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); directive = fixture.debugElement .query(By.directive(SamStickyComponent)) .injector.get(SamStickyComponent); }); it("should compile", () => { // Angular 20 removes the `ng-reflect-*` debug attributes this assertion // used to depend on (see AC 3 of GH-574: deprecated-API fixes from the // Angular 20 migration). Assert the real behaviour instead: the // directive received its bound `limit` input. expect(directive.limit).toBe(600); }); it("should handle when resized", () => { directive.resize(new Event("resize")); const comp = fixture.debugElement.query(By.css(".test-comp")); fixture.detectChanges(); expect(comp.nativeElement.getAttribute("style")).toContain( "position: static" ); }); it("trigger on scroll", () => { const expectedLimit = 1400; vi.spyOn(directive, "scroll"); window.dispatchEvent(new Event("scroll")); fixture.detectChanges(); expect(directive.scroll).toHaveBeenCalled(); directive.scroll(new Event("scroll")); directive.makeSticky(); directive.limit = expectedLimit; directive.makeSticky(); }); it("resize() recalculates elemWidth and calls makeSticky", () => { const makeStickySpy = vi.spyOn(directive, "makeSticky"); directive.resize(new Event("resize")); expect(makeStickySpy).toHaveBeenCalled(); const comp = fixture.debugElement.query(By.css(".test-comp")); expect(comp.nativeElement.style.position).toBe("static"); }); it("ngOnInit and ngAfterViewChecked pick up the element's current offsetWidth for later sticky positioning", () => { const containerEl = fixture.debugElement.query( By.css(".test-container") ).nativeElement; const compEl = fixture.debugElement.query( By.css(".test-comp") ).nativeElement; const siblingEl = containerEl.children[1]; Object.defineProperty(compEl, "offsetWidth", { value: 250, configurable: true, }); // Pick up the new offsetWidth via the public lifecycle hooks. directive.ngOnInit(); directive.ngAfterViewChecked(); // Force the sticky-fixed branch so the picked-up width is applied. Object.defineProperty(siblingEl, "offsetHeight", { value: 5000, configurable: true, }); Object.defineProperty(compEl, "offsetHeight", { value: 50, configurable: true, }); Object.defineProperty(compEl, "offsetTop", { value: 0, configurable: true, }); Object.defineProperty(containerEl, "offsetHeight", { value: 5000, configurable: true, }); Object.defineProperty(containerEl, "offsetTop", { value: 0, configurable: true, }); Object.defineProperty(window, "pageYOffset", { value: 200, configurable: true, }); directive.adjustStickyPos(); expect(compEl.style.width).toBe("250px"); Object.defineProperty(window, "pageYOffset", { value: 0, configurable: true, }); }); it("getDocHeight returns the largest of body/documentElement height metrics", () => { expect(typeof directive.getDocHeight()).toBe("number"); }); it("getScrollTop returns a number derived from pageYOffset and clientTop", () => { expect(typeof directive.getScrollTop()).toBe("number"); }); it("getElemDistanceToTop walks offsetParent chain and sums offsetTop", () => { const nativeElement = fixture.debugElement.query( By.css(".test-comp") ).nativeElement; expect(directive.getElemDistanceToTop(nativeElement)).toBe(0); const fakeParent: OffsetParentLike = { offsetTop: 40, offsetParent: null }; const fakeElem: OffsetParentLike = { offsetTop: 10, offsetParent: fakeParent as unknown as Element, }; expect(directive.getElemDistanceToTop(fakeElem)).toBe(50); }); describe("isTallestAmongSiblings / adjustStickyPos", () => { it("stays static when the directive's direct child is the tallest sibling", () => { directive.adjustStickyPos(); const comp = fixture.debugElement.query(By.css(".test-comp")); expect(comp.nativeElement.style.position).toBe("static"); }); it("goes fixed and sets top/width when scrolled past a shorter sibling", () => { const containerEl = fixture.debugElement.query( By.css(".test-container") ).nativeElement; const compEl = fixture.debugElement.query( By.css(".test-comp") ).nativeElement; const siblingEl = containerEl.children[1]; Object.defineProperty(siblingEl, "offsetHeight", { value: 5000, configurable: true, }); Object.defineProperty(compEl, "offsetHeight", { value: 50, configurable: true, }); Object.defineProperty(compEl, "offsetTop", { value: 0, configurable: true, }); Object.defineProperty(containerEl, "offsetHeight", { value: 5000, configurable: true, }); Object.defineProperty(containerEl, "offsetTop", { value: 0, configurable: true, }); Object.defineProperty(window, "pageYOffset", { value: 200, configurable: true, }); directive.adjustStickyPos(); expect(compEl.style.position).toBe("fixed"); expect(compEl.style.width).toContain("px"); expect(compEl.style.top).toContain("px"); Object.defineProperty(window, "pageYOffset", { value: 0, configurable: true, }); }); }); });