Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 129 additions & 6 deletions src/ui-kit/directives/drag-drop/drag-drop.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { TestBed, waitForAsync, fakeAsync, tick } from "@angular/core/testing";
import { TestBed } from "@angular/core/testing";

import { Component, Output, ViewChild, EventEmitter } from "@angular/core";
import { By } from "@angular/platform-browser";

// Load the implementations that should be tested
import { SamDragDropDirective } from "./drag-drop.directive";
import { SamDragDropDirective, DragState } from "./drag-drop.directive";

interface FakeDragEvent {
preventDefault: () => void;
stopPropagation: () => void;
dataTransfer?: { dropEffect?: string; files?: unknown[] };
target?: EventTarget;
}

@Component({
selector: "test-cmp",
Expand All @@ -21,7 +28,7 @@ import { SamDragDropDirective } from "./drag-drop.directive";
standalone: false,
})
class TestComponent {
@Output() action: EventEmitter<any> = new EventEmitter<any>();
@Output() action: EventEmitter<boolean> = new EventEmitter<boolean>();
@ViewChild("var", { static: true }) var;
@ViewChild("dummydrop", { static: true }) dummydrop;
dropHandler() {
Expand Down Expand Up @@ -57,7 +64,7 @@ describe("The Sam Focus directive", () => {
component.action.subscribe((val) => {
expect(val).toBe(true);
});
directive.onWindowDrop(<any>{
directive.onWindowDrop(<FakeDragEvent>{
preventDefault: function () {},
stopPropagation: function () {},
dataTransfer: {
Expand All @@ -71,7 +78,7 @@ describe("The Sam Focus directive", () => {
expect(val).toBe(true);
});

directive.onWindowDragover(<any>{
directive.onWindowDragover(<FakeDragEvent>{
preventDefault: function () {},
stopPropagation: function () {},
dataTransfer: {
Expand All @@ -80,7 +87,7 @@ describe("The Sam Focus directive", () => {
target: component.dummydrop.nativeElement,
});

directive.onElementDragend(<any>{
directive.onElementDragend(<FakeDragEvent>{
preventDefault: function () {},
stopPropagation: function () {},
dataTransfer: {
Expand All @@ -89,4 +96,120 @@ describe("The Sam Focus directive", () => {
target: component.dummydrop.nativeElement,
});
});

it("should emit window drop event", () => {
const preventDefault = vi.fn();
const stopPropagation = vi.fn();
directive.onWindowDrop(<FakeDragEvent>{
preventDefault,
stopPropagation,
});
expect(preventDefault).toHaveBeenCalled();
expect(stopPropagation).toHaveBeenCalled();
});

it("should set drag state to NotDragging on element drag end", () => {
directive.dragState = DragState.DraggingInTarget;
directive.onElementDragend(<FakeDragEvent>{
preventDefault: () => undefined,
stopPropagation: () => undefined,
});
expect(directive.dragState).toBe(DragState.NotDragging);
});

describe("onElementDrop", () => {
it("does nothing but sets dropEffect to none when disabled", () => {
directive.disabled = true;
const event: FakeDragEvent = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
dataTransfer: { dropEffect: "", files: ["test.jpg"] },
target: component.dummydrop.nativeElement,
};
const dropSpy = vi.fn();
directive.dropEvent.subscribe(dropSpy);

directive.onElementDrop(event);

expect(event.dataTransfer.dropEffect).toBe("none");
expect(dropSpy).not.toHaveBeenCalled();
});

it("emits dropEvent with files when the drop is inside the target with files", () => {
const files = ["test.jpg"];
const event: FakeDragEvent = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
dataTransfer: { dropEffect: "", files },
target: component.dummydrop.nativeElement,
};
const dropSpy = vi.fn();
directive.dropEvent.subscribe(dropSpy);

directive.onElementDrop(event);

expect(dropSpy).toHaveBeenCalledWith(files);
expect(directive.dragState).toBe(DragState.NotDragging);
});

it("does not emit dropEvent when the drop target has no files", () => {
const event: FakeDragEvent = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
dataTransfer: { dropEffect: "", files: [] },
target: component.dummydrop.nativeElement,
};
const dropSpy = vi.fn();
directive.dropEvent.subscribe(dropSpy);

directive.onElementDrop(event);

expect(dropSpy).not.toHaveBeenCalled();
});
});

describe("onElementDragOver", () => {
it("sets dropEffect to none and skips processing when disabled", () => {
directive.disabled = true;
const event: FakeDragEvent = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
dataTransfer: { dropEffect: "" },
target: component.dummydrop.nativeElement,
};

directive.onElementDragOver(event);

expect(event.dataTransfer.dropEffect).toBe("none");
});

it("sets DraggingInTarget state and copy dropEffect when dragging inside the target", () => {
const event: FakeDragEvent = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
dataTransfer: { dropEffect: "" },
target: component.dummydrop.nativeElement,
};

directive.onElementDragOver(event);

expect(directive.dragState).toBe(DragState.DraggingInTarget);
expect(event.dataTransfer.dropEffect).toBe("copy");
});

it("sets DraggingOutsideTarget state and none dropEffect when dragging outside the target", () => {
const outsideElement = document.createElement("div");
const event: FakeDragEvent = {
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
dataTransfer: { dropEffect: "" },
target: outsideElement,
};

directive.onElementDragOver(event);

expect(directive.dragState).toBe(DragState.DraggingOutsideTarget);
expect(event.dataTransfer.dropEffect).toBe("none");
});
});
});
136 changes: 136 additions & 0 deletions src/ui-kit/directives/sticky/sticky.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,140 @@ describe("The Sam Sticky directive", () => {
directive.limit = expectedLimit;
directive.makeSticky();
});

it("resize() recalculates elemWidth and calls makeSticky", () => {
const makeStickySpy = vi.spyOn(directive, "makeSticky");
directive.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 = { offsetTop: 40, offsetParent: null };
const fakeElem = { offsetTop: 10, offsetParent: fakeParent };
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,
});
});
});
});
66 changes: 66 additions & 0 deletions src/ui-kit/dom-helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ScrollHelpers } from "./dom-helpers";

describe("ScrollHelpers", () => {
let originalOnWheel: typeof window.onwheel;
let originalOnKeyDown: typeof document.onkeydown;

beforeEach(() => {
originalOnWheel = window.onwheel;
originalOnKeyDown = document.onkeydown;
});

afterEach(() => {
window.onwheel = originalOnWheel;
document.onkeydown = originalOnKeyDown;
});

it("returns undefined when window is falsy", () => {
expect(ScrollHelpers(undefined)).toBeUndefined();
});

it("returns disableScroll/enableScroll functions for a window object", () => {
const helpers = ScrollHelpers(window);
expect(typeof helpers.disableScroll).toBe("function");
expect(typeof helpers.enableScroll).toBe("function");
});

it("disableScroll wires up scroll-blocking handlers and enableScroll clears them", () => {
const helpers = ScrollHelpers(window);

helpers.disableScroll();
expect(typeof window.onwheel).toBe("function");
expect(typeof document.onkeydown).toBe("function");

helpers.enableScroll();
expect(window.onwheel).toBeFalsy();
expect(document.onkeydown).toBeFalsy();
});

it("preventDefaultForScrollKeys prevents default for arrow keys", () => {
const helpers = ScrollHelpers(window);
helpers.disableScroll();

const preventDefault = vi.fn();
(document.onkeydown as (e: KeyboardEvent) => void)({
keyCode: 37,
preventDefault,
} as unknown as KeyboardEvent);
expect(preventDefault).toHaveBeenCalled();

helpers.enableScroll();
});

it("does nothing for keys that are not scroll keys", () => {
const helpers = ScrollHelpers(window);
helpers.disableScroll();

const preventDefault = vi.fn();
const result = (
document.onkeydown as (e: KeyboardEvent) => boolean | undefined
)({ keyCode: 65, preventDefault } as unknown as KeyboardEvent);
expect(preventDefault).not.toHaveBeenCalled();
expect(result).toBeUndefined();

helpers.enableScroll();
});
});
Loading
Loading