import { FormFieldElement } from "../types"; import DomElementVisibilityService from "./dom-element-visibility.service"; function createBoundingClientRectMock(customProperties: Partial = {}): DOMRectReadOnly { return { top: 0, bottom: 0, left: 0, right: 0, width: 500, height: 500, x: 0, y: 0, toJSON: jest.fn(), ...customProperties, }; } describe("DomElementVisibilityService", () => { let domElementVisibilityService: DomElementVisibilityService; beforeEach(() => { document.body.innerHTML = `
`; domElementVisibilityService = new DomElementVisibilityService(); }); afterEach(() => { jest.clearAllMocks(); document.body.innerHTML = ""; }); describe("isFormFieldViewable", () => { it("returns false if the element is outside viewport bounds", async () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; jest.spyOn(usernameElement, "getBoundingClientRect"); jest .spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds") .mockResolvedValueOnce(true); jest.spyOn(domElementVisibilityService, "isElementHiddenByCss"); jest.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement"); const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable(usernameElement); expect(isFormFieldViewable).toEqual(false); expect(usernameElement.getBoundingClientRect).toHaveBeenCalled(); expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith( usernameElement, usernameElement.getBoundingClientRect(), ); expect(domElementVisibilityService["isElementHiddenByCss"]).not.toHaveBeenCalled(); expect( domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"], ).not.toHaveBeenCalled(); }); it("returns false if the element is hidden by CSS", async () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; jest.spyOn(usernameElement, "getBoundingClientRect"); jest .spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds") .mockReturnValueOnce(false); jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(true); jest.spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement"); const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable(usernameElement); expect(isFormFieldViewable).toEqual(false); expect(usernameElement.getBoundingClientRect).toHaveBeenCalled(); expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith( usernameElement, usernameElement.getBoundingClientRect(), ); expect(domElementVisibilityService["isElementHiddenByCss"]).toHaveBeenCalledWith( usernameElement, ); expect( domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"], ).not.toHaveBeenCalled(); }); it("returns false if the element is hidden behind another element", async () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; jest.spyOn(usernameElement, "getBoundingClientRect"); jest .spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds") .mockReturnValueOnce(false); jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(false); jest .spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement") .mockReturnValueOnce(false); const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable(usernameElement); expect(isFormFieldViewable).toEqual(false); expect(usernameElement.getBoundingClientRect).toHaveBeenCalled(); expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith( usernameElement, usernameElement.getBoundingClientRect(), ); expect(domElementVisibilityService["isElementHiddenByCss"]).toHaveBeenCalledWith( usernameElement, ); expect( domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"], ).toHaveBeenCalledWith(usernameElement, usernameElement.getBoundingClientRect()); }); it("returns true if the form field is viewable", async () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; jest.spyOn(usernameElement, "getBoundingClientRect"); jest .spyOn(domElementVisibilityService as any, "isElementOutsideViewportBounds") .mockReturnValueOnce(false); jest.spyOn(domElementVisibilityService, "isElementHiddenByCss").mockReturnValueOnce(false); jest .spyOn(domElementVisibilityService as any, "formFieldIsNotHiddenBehindAnotherElement") .mockReturnValueOnce(true); const isFormFieldViewable = await domElementVisibilityService.isFormFieldViewable(usernameElement); expect(isFormFieldViewable).toEqual(true); expect(usernameElement.getBoundingClientRect).toHaveBeenCalled(); expect(domElementVisibilityService["isElementOutsideViewportBounds"]).toHaveBeenCalledWith( usernameElement, usernameElement.getBoundingClientRect(), ); expect(domElementVisibilityService["isElementHiddenByCss"]).toHaveBeenCalledWith( usernameElement, ); expect( domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"], ).toHaveBeenCalledWith(usernameElement, usernameElement.getBoundingClientRect()); }); }); describe("isElementHiddenByCss", () => { it("returns true when a non-hidden element is passed", () => { document.body.innerHTML = ` `; const usernameElement = document.getElementById("username"); const isElementHidden = domElementVisibilityService["isElementHiddenByCss"](usernameElement); expect(isElementHidden).toEqual(false); }); it("returns true when the element has a `visibility: hidden;` CSS rule applied to it either inline or in a computed style", () => { document.body.innerHTML = ` `; const usernameElement = document.getElementById("username"); const passwordElement = document.getElementById("password"); jest.spyOn(usernameElement.style, "getPropertyValue"); jest.spyOn(usernameElement.ownerDocument.defaultView, "getComputedStyle"); jest.spyOn(passwordElement.style, "getPropertyValue"); jest.spyOn(passwordElement.ownerDocument.defaultView, "getComputedStyle"); const isUsernameElementHidden = domElementVisibilityService["isElementHiddenByCss"](usernameElement); const isPasswordElementHidden = domElementVisibilityService["isElementHiddenByCss"](passwordElement); expect(isUsernameElementHidden).toEqual(true); expect(usernameElement.style.getPropertyValue).toHaveBeenCalled(); expect(usernameElement.ownerDocument.defaultView.getComputedStyle).toHaveBeenCalledWith( usernameElement, ); expect(isPasswordElementHidden).toEqual(true); expect(passwordElement.style.getPropertyValue).toHaveBeenCalled(); expect(passwordElement.ownerDocument.defaultView.getComputedStyle).toHaveBeenCalledWith( passwordElement, ); }); it("returns true when the element has a `display: none;` CSS rule applied to it either inline or in a computed style", () => { document.body.innerHTML = ` `; const usernameElement = document.getElementById("username"); const passwordElement = document.getElementById("password"); const isUsernameElementHidden = domElementVisibilityService["isElementHiddenByCss"](usernameElement); const isPasswordElementHidden = domElementVisibilityService["isElementHiddenByCss"](passwordElement); expect(isUsernameElementHidden).toEqual(true); expect(isPasswordElementHidden).toEqual(true); }); it("returns true when the element has a `opacity: 0;` CSS rule applied to it either inline or in a computed style", () => { document.body.innerHTML = ` `; const usernameElement = document.getElementById("username"); const passwordElement = document.getElementById("password"); const isUsernameElementHidden = domElementVisibilityService["isElementHiddenByCss"](usernameElement); const isPasswordElementHidden = domElementVisibilityService["isElementHiddenByCss"](passwordElement); expect(isUsernameElementHidden).toEqual(true); expect(isPasswordElementHidden).toEqual(true); }); it("returns true when the element has a `clip-path` CSS rule applied to it that hides the element either inline or in a computed style", () => { document.body.innerHTML = ` `; }); }); describe("isElementOutsideViewportBounds", () => { const mockViewportWidth = 1920; const mockViewportHeight = 1080; beforeEach(() => { Object.defineProperty(document.documentElement, "scrollWidth", { writable: true, value: mockViewportWidth, }); Object.defineProperty(document.documentElement, "scrollHeight", { writable: true, value: mockViewportHeight, }); }); it("returns true if the passed element's size is not sufficient for visibility", () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; const elementBoundingClientRect = createBoundingClientRectMock({ width: 9, height: 9, }); const isElementOutsideViewportBounds = domElementVisibilityService[ "isElementOutsideViewportBounds" ](usernameElement, elementBoundingClientRect); expect(isElementOutsideViewportBounds).toEqual(true); }); it("returns true if the passed element is overflowing the left viewport", () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; const elementBoundingClientRect = createBoundingClientRectMock({ left: -1, }); const isElementOutsideViewportBounds = domElementVisibilityService[ "isElementOutsideViewportBounds" ](usernameElement, elementBoundingClientRect); expect(isElementOutsideViewportBounds).toEqual(true); }); it("returns true if the passed element is overflowing the right viewport", () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; const elementBoundingClientRect = createBoundingClientRectMock({ left: mockViewportWidth + 1, }); const isElementOutsideViewportBounds = domElementVisibilityService[ "isElementOutsideViewportBounds" ](usernameElement, elementBoundingClientRect); expect(isElementOutsideViewportBounds).toEqual(true); }); it("returns true if the passed element is overflowing the top viewport", () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; const elementBoundingClientRect = createBoundingClientRectMock({ top: -1, }); const isElementOutsideViewportBounds = domElementVisibilityService[ "isElementOutsideViewportBounds" ](usernameElement, elementBoundingClientRect); expect(isElementOutsideViewportBounds).toEqual(true); }); it("returns true if the passed element is overflowing the bottom viewport", () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; const elementBoundingClientRect = createBoundingClientRectMock({ top: mockViewportHeight + 1, }); const isElementOutsideViewportBounds = domElementVisibilityService[ "isElementOutsideViewportBounds" ](usernameElement, elementBoundingClientRect); expect(isElementOutsideViewportBounds).toEqual(true); }); it("returns false if the passed element is not outside of the viewport bounds", () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; const elementBoundingClientRect = createBoundingClientRectMock({}); const isElementOutsideViewportBounds = domElementVisibilityService[ "isElementOutsideViewportBounds" ](usernameElement, elementBoundingClientRect); expect(isElementOutsideViewportBounds).toEqual(false); }); }); describe("formFieldIsNotHiddenBehindAnotherElement", () => { it("returns true if the element found at the center point of the passed targetElement is the targetElement itself", () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; jest.spyOn(usernameElement, "getBoundingClientRect"); document.elementFromPoint = jest.fn(() => usernameElement); const formFieldIsNotHiddenBehindAnotherElement = domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"](usernameElement); expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(true); expect(document.elementFromPoint).toHaveBeenCalled(); expect(usernameElement.getBoundingClientRect).toHaveBeenCalled(); }); it("returns true if the element found at the center point of the passed targetElement is an implicit label of the element", () => { document.body.innerHTML = ` `; const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; const labelTextElement = document.querySelector("span"); document.elementFromPoint = jest.fn(() => labelTextElement); const formFieldIsNotHiddenBehindAnotherElement = domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"](usernameElement); expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(true); }); it("returns true if the element found at the center point of the passed targetElement is a label of the targetElement", () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; const labelElement = document.querySelector("label[for='username']") as FormFieldElement; const mockBoundingRect = createBoundingClientRectMock({}); jest.spyOn(usernameElement, "getBoundingClientRect"); document.elementFromPoint = jest.fn(() => labelElement); const formFieldIsNotHiddenBehindAnotherElement = domElementVisibilityService[ "formFieldIsNotHiddenBehindAnotherElement" ](usernameElement, mockBoundingRect); expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(true); expect(document.elementFromPoint).toHaveBeenCalledWith( mockBoundingRect.left + mockBoundingRect.width / 2, mockBoundingRect.top + mockBoundingRect.height / 2, ); expect(usernameElement.getBoundingClientRect).not.toHaveBeenCalled(); }); it("returns false if the element found at the center point is not the passed targetElement or a label of that element", () => { const usernameElement = document.querySelector("input[name='username']") as FormFieldElement; document.elementFromPoint = jest.fn(() => document.createElement("div")); const formFieldIsNotHiddenBehindAnotherElement = domElementVisibilityService["formFieldIsNotHiddenBehindAnotherElement"](usernameElement); expect(formFieldIsNotHiddenBehindAnotherElement).toEqual(false); }); }); });