@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
463 lines (387 loc) • 12.8 kB
JavaScript
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;
}
});
});