UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

463 lines (387 loc) 12.8 kB
import * as chai from "chai"; import { chaiDom } from "../../../util/chai-dom.mjs"; import { initJSDOM } from "../../../util/jsdom.mjs"; import { ResizeObserverMock } from "../../../util/resize-observer.mjs"; const expect = chai.expect; chai.use(chaiDom); function dispatchPointerEvent(target, type, options = {}) { const event = new Event(type, { bubbles: true, cancelable: true, composed: true, }); const values = { button: 0, isPrimary: true, pointerId: 1, clientX: 0, clientY: 0, ...options, }; for (const [key, value] of Object.entries(values)) { Object.defineProperty(event, key, { configurable: true, value, }); } target.dispatchEvent(event); return event; } function mockScrollableElement(element, { clientWidth, scrollWidth, scrollLeft = 0 }) { let currentScrollLeft = scrollLeft; Object.defineProperty(element, "clientWidth", { configurable: true, value: clientWidth, }); Object.defineProperty(element, "scrollWidth", { configurable: true, value: scrollWidth, }); Object.defineProperty(element, "scrollLeft", { configurable: true, get: () => currentScrollLeft, set: (value) => { currentScrollLeft = value; }, }); } describe("Datatable drag scroll", function () { before(async function () { await initJSDOM(); await import("element-internals-polyfill").catch(() => {}); await import("../../../../source/components/datatable/datatable.mjs"); }); beforeEach(() => { document.getElementById("mocks").innerHTML = ""; }); it("drags horizontally on non-interactive cells and suppresses the follow-up click", async function () { const mocks = document.getElementById("mocks"); const datatable = document.createElement("monster-datatable"); datatable.id = "drag-scroll-table"; datatable.innerHTML = ` <template id="drag-scroll-table-row"> <div data-monster-head="Name">Alpha</div> <div data-monster-head="Value">Beta</div> </template> `; datatable.setOption("data", [{}, {}]); mocks.appendChild(datatable); await new Promise((resolve) => setTimeout(resolve, 30)); const scroll = datatable.shadowRoot.querySelector( "[data-monster-role=table-scroll]", ); const cell = datatable.shadowRoot.querySelector( "[data-monster-role=datatable] > div", ); expect(scroll).to.exist; expect(cell).to.exist; mockScrollableElement(scroll, { clientWidth: 200, scrollWidth: 600, }); scroll.scrollLeft = 120; let clickCount = 0; cell.addEventListener("click", () => { clickCount += 1; }); dispatchPointerEvent(cell, "pointerdown", { clientX: 100, clientY: 20, }); dispatchPointerEvent(window, "pointermove", { clientX: 40, clientY: 24, }); dispatchPointerEvent(window, "pointerup", { clientX: 40, clientY: 24, }); expect(scroll.scrollLeft).to.equal(180); expect(scroll.classList.contains("is-dragging")).to.equal(false); cell.dispatchEvent( new window.MouseEvent("click", { bubbles: true, cancelable: true, composed: true, }), ); expect(clickCount).to.equal(0); }); it("does not break clicks on embedded controls", async function () { const mocks = document.getElementById("mocks"); const datatable = document.createElement("monster-datatable"); datatable.id = "drag-scroll-button-table"; datatable.innerHTML = ` <template id="drag-scroll-button-table-row"> <div data-monster-head="Name">Alpha</div> <div data-monster-head="Action"><button type="button">Open</button></div> </template> `; datatable.setOption("data", [{}]); mocks.appendChild(datatable); await new Promise((resolve) => setTimeout(resolve, 30)); const scroll = datatable.shadowRoot.querySelector( "[data-monster-role=table-scroll]", ); const button = datatable.shadowRoot.querySelector("button"); expect(scroll).to.exist; expect(button).to.exist; mockScrollableElement(scroll, { clientWidth: 200, scrollWidth: 600, }); scroll.scrollLeft = 120; let clickCount = 0; button.addEventListener("click", () => { clickCount += 1; }); dispatchPointerEvent(button, "pointerdown", { clientX: 100, clientY: 20, }); dispatchPointerEvent(window, "pointermove", { clientX: 30, clientY: 22, }); dispatchPointerEvent(window, "pointerup", { clientX: 30, clientY: 22, }); button.dispatchEvent( new window.MouseEvent("click", { bubbles: true, cancelable: true, composed: true, }), ); expect(scroll.scrollLeft).to.equal(120); expect(clickCount).to.equal(1); }); it("keeps double click copy for a single cell", async function () { const mocks = document.getElementById("mocks"); const datatable = document.createElement("monster-datatable"); datatable.id = "copy-cell-table"; datatable.innerHTML = ` <template id="copy-cell-table-row"> <div data-monster-head="Name">Alpha</div> <div data-monster-head="Value">Beta</div> </template> `; datatable.setOption("data", [{}]); mocks.appendChild(datatable); await new Promise((resolve) => setTimeout(resolve, 30)); const copied = []; Object.defineProperty(window.navigator, "clipboard", { configurable: true, value: { writeText: async (text) => { copied.push(text); }, }, }); const cell = datatable.shadowRoot.querySelector( "[data-monster-role=datatable] > div", ); cell.dispatchEvent( new window.MouseEvent("dblclick", { bubbles: true, cancelable: true, composed: true, }), ); await new Promise((resolve) => setTimeout(resolve, 0)); expect(copied).deep.equal(["Alpha"]); }); it("copies the whole row on shift click", async function () { const mocks = document.getElementById("mocks"); const datatable = document.createElement("monster-datatable"); datatable.id = "copy-row-table"; datatable.innerHTML = ` <template id="copy-row-table-row"> <div data-monster-head="Name">Alpha</div> <div data-monster-head="Value">Beta</div> </template> `; datatable.setOption("data", [{}]); mocks.appendChild(datatable); await new Promise((resolve) => setTimeout(resolve, 30)); const copied = []; Object.defineProperty(window.navigator, "clipboard", { configurable: true, value: { writeText: async (text) => { copied.push(text); }, }, }); const cell = datatable.shadowRoot.querySelector( "[data-monster-role=datatable] > div", ); cell.dispatchEvent( new window.MouseEvent("click", { bubbles: true, cancelable: true, composed: true, shiftKey: true, }), ); await new Promise((resolve) => setTimeout(resolve, 0)); expect(copied).deep.equal(['"Alpha";"Beta"']); }); it("defers resize observer grid updates to the next animation frame", async function () { const OriginalResizeObserver = window.ResizeObserver; const originalGlobalResizeObserver = globalThis.ResizeObserver; const originalRequestAnimationFrame = window.requestAnimationFrame; const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame; class TrackingResizeObserver extends ResizeObserverMock { static instances = []; constructor(callback) { super(callback); TrackingResizeObserver.instances.push(this); } } try { window.ResizeObserver = TrackingResizeObserver; globalThis.ResizeObserver = TrackingResizeObserver; const mocks = document.getElementById("mocks"); const wrapper = document.createElement("div"); let wrapperWidth = 500; Object.defineProperty(wrapper, "clientWidth", { configurable: true, get: () => wrapperWidth, }); mocks.appendChild(wrapper); const datatable = document.createElement("monster-datatable"); datatable.id = "resize-scheduled-table"; datatable.innerHTML = ` <template id="resize-scheduled-table-row"> <div data-monster-head="Name">Alpha</div> <div data-monster-head="Value">Beta</div> </template> `; datatable.setOption("responsive.breakpoint", 300); datatable.setOption("data", [{}]); wrapper.appendChild(datatable); await new Promise((resolve) => setTimeout(resolve, 30)); const control = datatable.shadowRoot.querySelector( "[data-monster-role=control]", ); expect(control).to.exist; expect(control.classList.contains("small")).to.equal(false); expect(TrackingResizeObserver.instances).to.have.length.greaterThan(0); const tableResizeObserver = TrackingResizeObserver.instances.find( (observer) => observer.elements.includes(wrapper), ); expect(tableResizeObserver).to.exist; let scheduledCallback = null; window.requestAnimationFrame = (callback) => { scheduledCallback = callback; return 1; }; globalThis.requestAnimationFrame = window.requestAnimationFrame; wrapperWidth = 200; tableResizeObserver.triggerResize([]); expect(scheduledCallback).to.be.a("function"); expect(control.classList.contains("small")).to.equal(false); scheduledCallback(); expect(control.classList.contains("small")).to.equal(true); } finally { window.ResizeObserver = OriginalResizeObserver; globalThis.ResizeObserver = originalGlobalResizeObserver; window.requestAnimationFrame = originalRequestAnimationFrame; globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame; } }); it("defers column bar resize updates to the next animation frame", async function () { const OriginalResizeObserver = window.ResizeObserver; const originalGlobalResizeObserver = globalThis.ResizeObserver; const originalRequestAnimationFrame = window.requestAnimationFrame; const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame; class TrackingResizeObserver extends ResizeObserverMock { static instances = []; constructor(callback) { super(callback); TrackingResizeObserver.instances.push(this); } } try { window.ResizeObserver = TrackingResizeObserver; globalThis.ResizeObserver = TrackingResizeObserver; const mocks = document.getElementById("mocks"); const wrapper = document.createElement("div"); let parentWidth = 400; Object.defineProperty(wrapper, "getBoundingClientRect", { configurable: true, value: () => ({ width: parentWidth }), }); mocks.appendChild(wrapper); const columnBar = document.createElement("monster-column-bar"); columnBar.setOption("columns", [ { index: 0, name: "One", visible: true }, { index: 1, name: "Two", visible: true }, { index: 2, name: "Three", visible: true }, { index: 3, name: "Four", visible: true }, { index: 4, name: "Five", visible: true }, { index: 5, name: "Six", visible: true }, ]); wrapper.appendChild(columnBar); await new Promise((resolve) => setTimeout(resolve, 30)); const control = columnBar.shadowRoot.querySelector( "[data-monster-role=control]", ); const settingsButton = columnBar.shadowRoot.querySelector( "[data-monster-role=settings-button]", ); const dotsContainer = columnBar.shadowRoot.querySelector( "[data-monster-role=dots]", ); const dots = Array.from(dotsContainer.querySelectorAll("li")); expect(control).to.exist; expect(settingsButton).to.exist; expect(dots.length).to.equal(6); Object.defineProperty(control, "getBoundingClientRect", { configurable: true, value: () => ({ width: parentWidth }), }); Object.defineProperty(settingsButton, "getBoundingClientRect", { configurable: true, value: () => ({ width: 20 }), }); dots.forEach((dot) => { Object.defineProperty(dot, "getBoundingClientRect", { configurable: true, value: () => ({ width: 20 }), }); }); await new Promise((resolve) => setTimeout(resolve, 0)); expect( dotsContainer.querySelector(".dots-overflow-indicator"), ).to.equal(null); const columnBarResizeObserver = TrackingResizeObserver.instances.find( (observer) => observer.elements.includes(control), ); expect(columnBarResizeObserver).to.exist; let scheduledCallback = null; window.requestAnimationFrame = (callback) => { scheduledCallback = callback; return 1; }; globalThis.requestAnimationFrame = window.requestAnimationFrame; parentWidth = 80; columnBarResizeObserver.triggerResize([]); expect(scheduledCallback).to.be.a("function"); expect( dotsContainer.querySelector(".dots-overflow-indicator"), ).to.equal(null); scheduledCallback(); expect( dotsContainer.querySelector(".dots-overflow-indicator"), ).to.exist; } finally { window.ResizeObserver = OriginalResizeObserver; globalThis.ResizeObserver = originalGlobalResizeObserver; window.requestAnimationFrame = originalRequestAnimationFrame; globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame; } }); });