Files
2026-07-13 12:25:07 +08:00

418 lines
16 KiB
TypeScript

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import {
test,
expect,
PageView,
compareContentsToSnapshot,
} from "../helpers.ts";
async function openSidebarAndScrollToBottom() {
const elem = document.querySelector("perspective-viewer");
await elem.getTable();
await elem.toggleConfig(true);
elem.shadowRoot.querySelector("#active-columns").scrollTop = 500;
}
async function checkSaveDisabled(page, expr) {
let view = new PageView(page);
let settingsPanel = await view.openSettingsPanel();
await settingsPanel.createNewExpression("", expr, false);
}
test.beforeEach(async ({ page }) => {
await page.goto(
"/node_modules/@perspective-dev/viewer/test/html/superstore.html",
);
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
await page.evaluate(async () => {
await document.querySelector("perspective-viewer").restore({
plugin: "Debug",
});
});
});
test.describe("Expressions", () => {
test("editor > opens on add-column button click", async ({ page }) => {
await page.evaluate(openSidebarAndScrollToBottom);
await page.waitForFunction(
() =>
!!document
.querySelector("perspective-viewer")
.shadowRoot.querySelector("#add-expression"),
);
await page.locator("perspective-viewer #add-expression").click();
await page.waitForFunction(() => {
const root = document
.querySelector("perspective-viewer")
.shadowRoot.querySelector("#editor-container");
return !!root;
});
const editor = await page.waitForFunction(async () => {
const root = document
.querySelector("perspective-viewer")
.shadowRoot.querySelector("#editor-container");
return root.querySelector("#content");
});
const contents = await editor.evaluate((x) => x.outerHTML);
await page.evaluate(() => document.activeElement.blur());
await compareContentsToSnapshot(contents);
});
test("editor > closes on close button click", async ({ page }) => {
await page.evaluate(openSidebarAndScrollToBottom);
await page.waitForFunction(
() =>
!!document
.querySelector("perspective-viewer")
.shadowRoot.querySelector("#add-expression"),
);
await page.locator("perspective-viewer #add-expression").click();
await page.waitForSelector("#editor-container");
await page.evaluate(async () => {
let root = document.querySelector("perspective-viewer").shadowRoot;
await root.querySelector("#column_settings_close_button").click();
});
await page.waitForSelector("#editor-container", {
state: "hidden",
});
const contents = await page.evaluate(async () => {
let root = document.querySelector("perspective-viewer").shadowRoot;
return (
root.querySelector("#editor-container")?.innerHTML || "MISSING"
);
});
await compareContentsToSnapshot(contents);
});
test("validation > disables save for unknown symbols", async ({ page }) => {
await checkSaveDisabled(page, "abc");
});
test("validation > disables save for type-invalid expression", async ({
page,
}) => {
await checkSaveDisabled(page, '"Sales" + "Category";');
});
test("validation > disables save for invalid column references", async ({
page,
}) => {
await checkSaveDisabled(page, '"aaaa" + "Sales";');
});
test("columns > shows both aliased and non-aliased expressions", async ({
page,
}) => {
const contents = await page.evaluate(async () => {
document.activeElement.blur();
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
expressions: { "1 + 2": "1 + 2", abc: "3 + 4" },
});
return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
});
await compareContentsToSnapshot(contents);
});
// No longer relevant as we cannot save a duplicate identifier
test.skip("columns > overwrites a duplicate expression alias", async ({
page,
}) => {
let view = new PageView(page);
view.restore({ expressions: { "4 + 5": "3 + 4" } });
await view.settingsPanel.createNewExpression("", "4 + 5", true);
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
});
await compareContentsToSnapshot(contents);
});
// No longer relevant as we cannot save a duplicate identifier
test.skip("columns > overwrites a duplicate expression", async ({
page,
}) => {
let view = new PageView(page);
view.restore({ expressions: { "3 + 4": "3 + 4" } });
await view.settingsPanel.createNewExpression("", "3 + 4", true);
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
});
await compareContentsToSnapshot(contents);
});
test("reset > deletes all expressions on full reset", async ({ page }) => {
await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
expressions: { "3 + 4": "3 + 4", "1 + 2": "1 + 2" },
});
await elem.reset(true);
});
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return (
elem.shadowRoot.querySelector("#sub-columns")?.innerHTML ||
"MISSING"
);
});
await compareContentsToSnapshot(contents);
});
test("reset > preserves expressions on partial reset", async ({ page }) => {
await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
expressions: { "3 + 4": "3 + 4", "1 + 2": "1 + 2" },
});
await elem.reset(false);
});
const content = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
});
await compareContentsToSnapshot(content);
});
test("reset > deletes expressions used in columns on full reset", async ({
page,
}) => {
await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
columns: ["1 + 2"],
expressions: { "3 + 4": "3 + 4", "1 + 2": "1 + 2" },
});
await elem.reset(true);
});
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return (
elem.shadowRoot.querySelector("#sub-columns")?.innerHTML ||
"MISSING"
);
});
await compareContentsToSnapshot(contents);
});
test("reset > preserves expressions used in columns on partial reset", async ({
page,
}) => {
await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
columns: ["1 + 2"],
expressions: { "3 + 4": "3 + 4", "1 + 2": "1 + 2" },
});
await elem.reset(false);
});
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
});
await compareContentsToSnapshot(contents);
});
test("reset > deletes expressions used in group_by/sort/filter on full reset", async ({
page,
}) => {
await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
columns: ["1 + 2"],
group_by: ["3 + 4"],
sort: [["1 + 2", "asc"]],
filter: [["1 + 2", "==", 3]],
expressions: { "3 + 4": "3 + 4", "1 + 2": "1 + 2" },
});
await elem.reset(true);
});
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return (
elem.shadowRoot.querySelector("#sub-columns")?.innerHTML ||
"MISSING"
);
});
await compareContentsToSnapshot(contents);
});
test("persistence > survives views that don't reference them", async ({
page,
}) => {
await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
expressions: { "3 + 4": "3 + 4", "1 + 2": "1 + 2" },
});
await elem.restore({
columns: ["State"],
});
});
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
});
await compareContentsToSnapshot(contents);
});
test("add button > marks on hover", async ({ page }) => {
await page.evaluate(openSidebarAndScrollToBottom);
let addExprButton = await page.waitForSelector("#add-expression");
let notHovered = await addExprButton.getAttribute("class");
expect(notHovered).toBeNull();
await addExprButton.hover();
let hovered = await addExprButton.getAttribute("class");
expect(hovered).toBe("dragdrop-hover");
});
// Currently does not work in Firefox!
test("add button > marks on click", async ({ page }) => {
await page.evaluate(openSidebarAndScrollToBottom);
let addExprButton = await page.waitForSelector("#add-expression");
let unclicked = await addExprButton.getAttribute("class");
expect(unclicked).toBeNull();
await addExprButton.click();
await page.evaluate(openSidebarAndScrollToBottom);
let clicked = await addExprButton.getAttribute("class");
expect(clicked).toBe("dragdrop-hover");
});
test("persistence > survives views that reference them", async ({
page,
}) => {
await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
expressions: { "3 + 4": "3 + 4", "1 + 2": "1 + 2" },
});
await elem.restore({
columns: ["3 + 4"],
});
});
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
});
await compareContentsToSnapshot(contents);
});
test("aggregates > applies aggregate to expression column", async ({
page,
}) => {
await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
expressions: { '"Sales" + 100': '"Sales" + 100' },
aggregates: { '"Sales" + 100': "avg" },
group_by: ["State"],
columns: ['"Sales" + 100'],
});
});
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
});
await compareContentsToSnapshot(contents);
});
test("sort > sorts by a hidden expression column", async ({ page }) => {
await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
expressions: { '"Sales" + 100': '"Sales" + 100' },
sort: [['"Sales" + 100', "asc"]],
columns: ["Row ID"],
});
});
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
});
await compareContentsToSnapshot(contents);
});
test("filter > filters by an expression column", async ({ page }) => {
await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
await elem.toggleConfig(true);
await elem.restore({
expressions: { '"Sales" + 100': '"Sales" + 100' },
filter: [['"Sales" + 100', ">", 150]],
columns: ["Row ID", '"Sales" + 100'],
});
});
const contents = await page.evaluate(async () => {
const elem = document.querySelector("perspective-viewer");
return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
});
await compareContentsToSnapshot(contents);
});
});