/*
* Copyright 2016 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it } from "@blueprintjs/test-commons/vitest";
import { Grid } from "./common/grid";
import { type Locator, LocatorImpl } from "./locator";
import { Utils } from ".";
const N_ROWS = 10;
const N_COLS = 10;
const ROW_HEIGHT = 10;
const COL_WIDTH = 20;
describe("Locator", () => {
const test10s = Utils.times(N_ROWS, () => ROW_HEIGHT);
const test20s = Utils.times(N_COLS, () => COL_WIDTH);
const grid = new Grid(test10s, test20s);
let locator: Locator;
let containerElement: HTMLElement;
beforeEach(() => {
// for some reason, the height is only 18px by default. need to manually increase it to fit
// all rows, which is necessary for certain row tests to pass.
//
// we also add room for one additional row to verify that certain behavior works when
// extending beyond the final row or column while still being within the table.
//
// finally, might as well explicitly set the width to make sure row and column tests operate
// with the same initial conditions.
const style = {
height: (N_ROWS + 1) * ROW_HEIGHT,
width: (N_COLS + 1) * COL_WIDTH,
};
// mount in the DOM to let us test scrolling behavior.
// ".body" will be the scrollable region.
const { container } = render(
,
);
containerElement = container;
// jsdom has no layout engine, so getBoundingClientRect returns all zeros.
// Mock it on the 3 elements to return realistic dimensions matching the styled sizes.
const mockRect = {
bottom: (N_ROWS + 1) * ROW_HEIGHT,
height: (N_ROWS + 1) * ROW_HEIGHT,
left: 0,
right: (N_COLS + 1) * COL_WIDTH,
toJSON: () => ({}),
top: 0,
width: (N_COLS + 1) * COL_WIDTH,
x: 0,
y: 0,
} as DOMRect;
containerElement.querySelector(".table-wrapper")!.getBoundingClientRect = () => mockRect;
containerElement.querySelector(".body")!.getBoundingClientRect = () => mockRect;
containerElement.querySelector(".body-client")!.getBoundingClientRect = () => mockRect;
locator = new LocatorImpl(
container.querySelector(".table-wrapper")!,
container.querySelector(".body")!,
container.querySelector(".body-client")!,
);
locator.setGrid(grid);
});
it("constructs", () => {
// noop
});
describe("convertPointToColumn", () => {
describe("when useMidpoint = false", () => {
it("locates a column", () => {
const left = containerElement.querySelector(".body")!.getBoundingClientRect().left;
expect(locator.convertPointToColumn(left + 10)).to.equal(0);
expect(locator.convertPointToColumn(left + 30)).to.equal(1);
expect(locator.convertPointToColumn(-1000)).to.equal(-1);
});
});
runTestSuiteForConvertPointToRowOrColumn(COL_WIDTH, N_COLS, "convertPointToColumn");
});
describe("convertPointToRow", () => {
describe("when useMidpoint = false", () => {
it("locates a row", () => {
const top = containerElement.querySelector(".body")!.getBoundingClientRect().top;
expect(locator.convertPointToRow(top + 5)).to.equal(0);
expect(locator.convertPointToRow(top + 15)).to.equal(1);
expect(locator.convertPointToRow(top + N_ROWS * ROW_HEIGHT - ROW_HEIGHT / 2)).to.equal(N_ROWS - 1);
expect(locator.convertPointToRow(-1000)).to.equal(-1);
});
});
runTestSuiteForConvertPointToRowOrColumn(ROW_HEIGHT, N_ROWS, "convertPointToRow");
});
describe("convertPointToCell", () => {
// skip: requires real browser layout engine for scroll behavior (jsdom limitation)
describe.skip("with frozen quadrants", () => {
let bodyElement: HTMLElement;
let originalOverflow: string;
let originalHeight: string;
let originalWidth: string;
let originalScrollLeft: number;
let originalScrollTop: number;
const NUM_FROZEN_COLUMNS = 1;
const NUM_FROZEN_ROWS = 1;
const NUM_COLUMNS_SCROLLED_OUT_OF_VIEW = 1;
const NUM_ROWS_SCROLLED_OUT_OF_VIEW = 1;
beforeEach(() => {
bodyElement = containerElement.querySelector(".body")!;
originalOverflow = bodyElement.style.overflow;
originalHeight = bodyElement.style.height;
originalWidth = bodyElement.style.width;
originalScrollLeft = bodyElement.scrollLeft;
originalScrollTop = bodyElement.scrollTop;
// make the table smaller, then scroll it one column and one row over
bodyElement.style.height = `${(N_ROWS / 2) * ROW_HEIGHT}px`;
bodyElement.style.width = `${(N_COLS / 2) * COL_WIDTH}px`;
bodyElement.style.overflow = "auto";
bodyElement.scrollLeft = NUM_COLUMNS_SCROLLED_OUT_OF_VIEW * COL_WIDTH;
bodyElement.scrollTop = NUM_ROWS_SCROLLED_OUT_OF_VIEW * ROW_HEIGHT;
});
afterEach(() => {
locator.setNumFrozenColumns(0);
locator.setNumFrozenRows(0);
bodyElement.scrollLeft = originalScrollLeft;
bodyElement.scrollTop = originalScrollTop;
bodyElement.style.overflow = originalOverflow;
bodyElement.style.width = originalWidth;
bodyElement.style.height = originalHeight;
});
describe("when table is scrolled downward and rightward", () => {
describe("with frozen column(s) only", () => {
beforeEach(() => {
locator.setNumFrozenColumns(NUM_FROZEN_COLUMNS);
});
it("locates a cell in the frozen LEFT quadrant", () => {
const { x, y } = getUnscrolledCellCoords(0, 0);
// frozen column still moves vertically on scroll
assertCellLocatedProperly(x, y, NUM_ROWS_SCROLLED_OUT_OF_VIEW, 0);
});
it("locates a scrolled cell in the MAIN quadrant", () => {
const lastFrozenIndex = NUM_FROZEN_COLUMNS - 1;
const unfrozenIndex = lastFrozenIndex + 1;
const { x, y } = getUnscrolledCellCoords(0, unfrozenIndex);
// unfrozen column moves horizontall and vertically on scroll
const expectedRow = NUM_ROWS_SCROLLED_OUT_OF_VIEW;
const expectedCol = unfrozenIndex + NUM_ROWS_SCROLLED_OUT_OF_VIEW;
assertCellLocatedProperly(x, y, expectedRow, expectedCol);
});
});
describe("with frozen rows(s) only", () => {
beforeEach(() => {
locator.setNumFrozenRows(NUM_FROZEN_ROWS);
});
it("locates a cell in the frozen TOP quadrant", () => {
const { x, y } = getUnscrolledCellCoords(0, 0);
// frozen row still moves horizontally on scroll
assertCellLocatedProperly(x, y, 0, NUM_COLUMNS_SCROLLED_OUT_OF_VIEW);
});
it("locates a scrolled cell in the MAIN quadrant", () => {
const lastFrozenIndex = NUM_FROZEN_ROWS - 1;
const unfrozenIndex = lastFrozenIndex + 1;
const { x, y } = getUnscrolledCellCoords(unfrozenIndex, 0);
// unfrozen column moves horizontall and vertically on scroll
const expectedRow = unfrozenIndex + NUM_COLUMNS_SCROLLED_OUT_OF_VIEW;
const expectedCol = NUM_COLUMNS_SCROLLED_OUT_OF_VIEW;
assertCellLocatedProperly(x, y, expectedRow, expectedCol);
});
});
describe("with frozen row(s) AND column(s)", () => {
beforeEach(() => {
locator.setNumFrozenRows(NUM_FROZEN_ROWS);
locator.setNumFrozenColumns(NUM_FROZEN_COLUMNS);
});
it("locates a cell in a frozen row AND column (TOP_LEFT quadrant)", () => {
const { x, y } = getUnscrolledCellCoords(0, 0);
// top-left frozen area doesn't move on scroll
assertCellLocatedProperly(x, y, 0, 0);
});
it("locates a scrolled cell in the MAIN quadrant", () => {
const lastFrozenRowIndex = NUM_FROZEN_ROWS - 1;
const lastFrozenColumnIndex = NUM_FROZEN_COLUMNS - 1;
const unfrozenRowIndex = lastFrozenRowIndex + 1;
const unfrozenColumnIndex = lastFrozenColumnIndex + 1;
const { x, y } = getUnscrolledCellCoords(unfrozenRowIndex, unfrozenColumnIndex);
// unfrozen column moves horizontall and vertically on scroll
const expectedRow = unfrozenRowIndex + NUM_COLUMNS_SCROLLED_OUT_OF_VIEW;
const expectedCol = unfrozenColumnIndex + NUM_COLUMNS_SCROLLED_OUT_OF_VIEW;
assertCellLocatedProperly(x, y, expectedRow, expectedCol);
});
});
});
});
});
function runTestSuiteForConvertPointToRowOrColumn(
elementSizeInPx: number,
nElements: number,
testFnName: "convertPointToColumn" | "convertPointToRow",
) {
const LAST_INDEX = nElements - 1;
describe("out of bounds", () => {
runTest(-100, -1);
runTest(-1, -1);
runTest((LAST_INDEX + 10) * elementSizeInPx, -1);
});
describe("snapping to index 0", () => {
runTest(0, 0);
runTest(getElementMidpoint(0), 0);
});
describe("snapping to index 1", () => {
runTest(getElementMidpointPlusOne(0), 1);
runTest(getElementMidpoint(1), 1);
});
describe("snapping to index 2", () => {
runTest(getElementMidpointPlusOne(1), 2);
});
describe("snapping to the last index", () => {
runTest(getElementMidpoint(LAST_INDEX), LAST_INDEX);
});
describe("snapping to the outer boundary of the last index", () => {
runTest(getElementMidpointPlusOne(LAST_INDEX), LAST_INDEX + 1);
// since we explicitly set the table width/height to fit one additional column/row, this
// coordinate should fall beyond the last column but still be within the table's
// bounding box.
runTest((LAST_INDEX + 1) * elementSizeInPx + 1, LAST_INDEX + 1);
});
function getElementMidpoint(elementIndex: number) {
const prevElementPixelOffset = elementIndex * elementSizeInPx;
const elementPixelOffset = (elementIndex + 1) * elementSizeInPx;
return (prevElementPixelOffset + elementPixelOffset) / 2;
}
function getElementMidpointPlusOne(elementIndex: number) {
return getElementMidpoint(elementIndex) + 1;
}
function runTest(clientCoord: number, expectedResult: number) {
it(`${clientCoord}px => ${expectedResult}`, () => {
const { top, left } = containerElement.querySelector(".body")!.getBoundingClientRect();
const baseOffset = testFnName === "convertPointToColumn" ? left : top;
const actualResult = locator[testFnName](baseOffset + clientCoord, true);
expect(actualResult).to.equal(expectedResult);
});
}
}
function assertCellLocatedProperly(clientX: number, clientY: number, expectedRow: number, expectedCol: number) {
const cell = locator.convertPointToCell(clientX, clientY);
expect(cell).to.deep.equal({ col: expectedCol, row: expectedRow });
}
function getUnscrolledCellCoords(row: number, col: number) {
const bodyRect = containerElement.querySelector(".body")!.getBoundingClientRect();
const colMidpointOffset = COL_WIDTH / 2;
const rowMidpointOffset = ROW_HEIGHT / 2;
// return the midpoint of the desired cell within the table container as if the table
// weren't scrolled
return {
x: bodyRect.left + col * COL_WIDTH + colMidpointOffset,
y: bodyRect.top + row * ROW_HEIGHT + rowMidpointOffset,
};
}
});