chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
@@ -0,0 +1,136 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 } from "@perspective-dev/test";
test.beforeEach(async ({ page }) => {
await page.goto("/tools/test/src/html/workspace-test.html");
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
});
test.describe("Context menu", () => {
test("shows tables in the New Table submenu", async ({ page }) => {
await page.evaluate(async () => {
await window.__WORKER__.table("x\n1\n2\n", {
name: "test_table_1",
});
await window.__WORKER__.table("y\n3\n4\n", {
name: "test_table_2",
});
});
// Right-click on the workspace to open context menu
const workspace = page.locator("perspective-workspace");
await workspace.click({
button: "right",
position: { x: 100, y: 100 },
});
// Wait for the context menu to appear in the shadow DOM
const shadowHost = page.locator("perspective-workspace");
const menu = shadowHost.locator(".lm-Menu").first();
await expect(menu).toBeVisible();
// Click on "New Table" to open the submenu
const newTableItem = menu.locator(
".lm-Menu-item:has(.lm-Menu-itemLabel:text('New Table'))",
);
await newTableItem.hover();
// Wait for submenu to appear
const submenu = shadowHost.locator(".lm-Menu").nth(1);
await expect(submenu).toBeVisible();
// Get the submenu content with table entries
const submenuContent = submenu.locator(".lm-Menu-content");
// Wait for the table items to be populated (they are added asynchronously)
// We expect at least our two test tables plus the default "superstore" table
await expect(submenuContent.locator("> .lm-Menu-item")).toHaveCount(3, {
timeout: 5000,
});
const menuItems = submenuContent.locator("> .lm-Menu-item");
const itemCount = await menuItems.count();
// Get the labels of the first items (the tables)
const labels = [];
for (let i = 0; i < itemCount; i++) {
const label = await menuItems
.nth(i)
.locator(".lm-Menu-itemLabel")
.textContent();
labels.push(label);
}
// Verify our test tables appear in the menu
expect(labels).toContain("test_table_1");
expect(labels).toContain("test_table_2");
expect(labels).toContain("superstore");
});
test("context menu table entries have correct structure", async ({
page,
}) => {
// Create two tables and load the workspace
await page.evaluate(async () => {
await window.__WORKER__.table("a\n1\n", { name: "alpha" });
await window.__WORKER__.table("b\n2\n", { name: "beta" });
});
// Right-click on the workspace
const workspace = page.locator("perspective-workspace");
await workspace.click({
button: "right",
position: { x: 100, y: 100 },
});
// Open the "New Table" submenu
const shadowHost = page.locator("perspective-workspace");
const menu = shadowHost.locator(".lm-Menu").first();
await expect(menu).toBeVisible();
const newTableItem = menu.locator(
".lm-Menu-item:has(.lm-Menu-itemLabel:text('New Table'))",
);
await newTableItem.hover();
const submenu = shadowHost.locator(".lm-Menu").nth(1);
await expect(submenu).toBeVisible();
// Verify the DOM structure of table entries
const submenuContent = submenu.locator(".lm-Menu-content");
// Wait for table items to be populated
await expect(submenuContent.locator("> .lm-Menu-item")).toHaveCount(3, {
timeout: 5000,
});
const firstItem = submenuContent.locator("> .lm-Menu-item").first();
// Check that the menu item has the expected Lumino classes
await expect(firstItem).toHaveClass(/lm-Menu-item/);
// Check that it contains a label element
const labelElement = firstItem.locator(".lm-Menu-itemLabel");
await expect(labelElement).toBeVisible();
// Verify the label contains one of our table names
const labelText = await labelElement.textContent();
expect(["alpha", "beta", "superstore"]).toContain(labelText);
});
});
+81
View File
@@ -0,0 +1,81 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 } from "@perspective-dev/test";
test.beforeEach(async ({ page }) => {
await page.goto("/tools/test/src/html/workspace-test.html");
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
});
test.describe("Workspace table functions", () => {
test("flush() waits until delete is resolved", async ({ page }) => {
const config = {
viewers: {
One: { table: "superstore", name: "One" },
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["One"],
},
},
};
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
const table = await window.__WORKER__.open_table("superstore");
await workspace.restore(config);
const viewer_removed =
workspace.querySelector("perspective-viewer");
workspace.removeChild(viewer_removed);
await viewer_removed.flush();
await table.delete();
}, config);
});
test("flush() waits until restore is applied and delete is resolved", async ({
page,
}) => {
const config = {
viewers: {
One: { table: "superstore", name: "One" },
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["One"],
},
},
};
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
const table = await window.__WORKER__.open_table("superstore");
workspace.restore(config); // no await
await workspace.flush();
const viewer_removed =
workspace.querySelector("perspective-viewer");
workspace.removeChild(viewer_removed);
await viewer_removed.flush();
await table.delete();
}, config);
});
});
+78
View File
@@ -0,0 +1,78 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 } from "@perspective-dev/test";
import {
compareLightDOMContents,
compareShadowDOMContents,
} from "@perspective-dev/test";
async function setupTestWorkspace(page) {
await page.goto("/tools/test/src/html/workspace-test.html");
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
await page.evaluate(async () => {
const viewer = document.createElement("perspective-viewer");
viewer.setAttribute("table", "superstore");
viewer.setAttribute("name", "one");
viewer.setAttribute("slot", "one");
const viewer2 = document.createElement("perspective-viewer");
viewer2.setAttribute("table", "superstore");
viewer2.setAttribute("name", "two");
viewer2.setAttribute("slot", "two");
const workspace = document.getElementById("workspace");
workspace.appendChild(viewer);
workspace.appendChild(viewer2);
await workspace.flush();
});
await page.evaluate(async () => {
const viewer = document.body.querySelector(
'perspective-viewer[name="one"]',
);
const workspace = document.getElementById("workspace");
workspace.removeChild(viewer);
await workspace.flush();
});
}
test.describe("Workspace DOM", () => {
test.describe("Light DOM", () => {
test.describe("removeChild", () => {
test("Remove One", async ({ page }) => {
await setupTestWorkspace(page);
await compareLightDOMContents(
page,
"workspace-light-remove-one-child.txt",
);
});
});
});
test.describe("Shadow DOM", () => {
test.describe("removeChild", () => {
test("Remove One", async ({ page }) => {
await setupTestWorkspace(page);
await compareShadowDOMContents(
page,
"workspace-dark-remove-one-child.txt",
);
});
});
});
});
@@ -0,0 +1,472 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 } from "@playwright/test";
import {
compareLightDOMContents,
compareShadowDOMContents,
} from "@perspective-dev/test";
test.beforeEach(async ({ page }) => {
await page.goto("/tools/test/src/html/workspace-test.html");
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
await page.evaluate(async () => {
const { PerspectiveSelectDetail } = await import(
"/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js"
);
window.PerspectiveSelectDetail = PerspectiveSelectDetail;
});
});
function tests(context, compare) {
// TODO: Implement this correctly
test.skip("treemap filters work", async ({ page }) => {
const config = {
viewers: {
One: {
table: "superstore",
name: "Test",
group_by: ["State"],
columns: ["Sales"],
plugin: "Treemap",
},
Two: { table: "superstore", name: "One" },
},
master: {
widgets: ["One"],
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["Two"],
},
},
};
const cfg = await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.restore(config);
await workspace.flush();
const timer = Promise.withResolvers();
workspace.addEventListener("workspace-layout-update", (x) => {
timer.resolve();
});
document
.querySelector("perspective-viewer-charts-treemap")
.shadowRoot.querySelector("g.treemap > g")
.dispatchEvent(new Event("click"));
await timer.promise;
return await workspace.save();
}, config);
expect(cfg.viewers.Two.filter).toEqual([["State", "==", "Alabama"]]);
return compare(page, `${context}-treemap-filters-work.txt`);
});
test("Datagrid filters work", async ({ page }) => {
const config = {
viewers: {
One: {
table: "superstore",
name: "Test",
group_by: ["State"],
columns: ["Sales"],
plugin: "Datagrid",
},
Two: { table: "superstore", name: "One" },
},
master: {
widgets: ["One"],
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["Two"],
},
},
};
async function awaitConfigChange() {
return await page.evaluate(async () => {
let resolve;
const timer = new Promise((x) => {
resolve = x;
});
workspace.addEventListener("workspace-layout-update", resolve);
await timer;
workspace.removeEventListener(
"workspace-layout-update",
resolve,
);
return await workspace.save();
});
}
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.restore(config);
await workspace.flush();
}, config);
let cfgPromise = awaitConfigChange();
await page
.locator(".workspace-master-widget perspective-viewer-datagrid")
.locator("tbody tr:nth-child(6) th:last-of-type")
.click();
let cfg = await cfgPromise;
expect(cfg.viewers.Two.filter).toEqual([["State", "==", "Colorado"]]);
cfgPromise = awaitConfigChange();
await page
.locator(".workspace-master-widget perspective-viewer-datagrid")
.locator("tbody tr:nth-child(6) th:last-of-type")
.click();
cfg = await cfgPromise;
expect(cfg.viewers.Two.filter).toEqual([]);
return compare(page, `${context}-datagrid-filters-work.txt`);
});
test("removeConfigs removes a programmatically applied filter from slave viewers", async ({
page,
}) => {
const config = {
viewers: {
One: {
table: "superstore",
name: "Test",
group_by: ["State"],
columns: ["Sales"],
plugin: "Datagrid",
},
Two: { table: "superstore", name: "One" },
},
master: {
widgets: ["One"],
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["Two"],
},
},
};
async function awaitConfigChange() {
return await page.evaluate(async () => {
let resolve;
const timer = new Promise((x) => {
resolve = x;
});
workspace.addEventListener("workspace-layout-update", resolve);
await timer;
workspace.removeEventListener(
"workspace-layout-update",
resolve,
);
return await workspace.save();
});
}
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.restore(config);
await workspace.flush();
}, config);
// Apply a filter for "Category" via programmatic dispatch.
// "Category" is not in the master's group_by/split_by/filter, so it
// would not be cleared by the candidates mechanism on deselect.
let cfgPromise = awaitConfigChange();
await page.evaluate(async () => {
const masterViewer = document.querySelector(
".workspace-master-widget",
);
masterViewer.dispatchEvent(
new CustomEvent("perspective-global-filter", {
bubbles: true,
composed: true,
detail: new PerspectiveSelectDetail(
true,
{},
["Category"],
[],
[{ filter: [["Category", "==", "Furniture"]] }],
),
}),
);
});
let cfg = await cfgPromise;
expect(cfg.viewers.Two.filter).toEqual([
["Category", "==", "Furniture"],
]);
// Use removeConfigs to explicitly clear the Category filter from
// slave viewers.
cfgPromise = awaitConfigChange();
await page.evaluate(async () => {
const masterViewer = document.querySelector(
".workspace-master-widget",
);
masterViewer.dispatchEvent(
new CustomEvent("perspective-global-filter", {
bubbles: true,
composed: true,
detail: new PerspectiveSelectDetail(
true,
{},
[],
[{ filter: [["Category", "==", "Furniture"]] }],
[],
),
}),
);
});
cfg = await cfgPromise;
expect(cfg.viewers.Two.filter).toEqual([]);
});
test("removeConfigs preserves other slave filters while clearing targeted column", async ({
page,
}) => {
const config = {
viewers: {
One: {
table: "superstore",
name: "Test",
group_by: ["State"],
columns: ["Sales"],
plugin: "Datagrid",
},
Two: { table: "superstore", name: "One" },
},
master: {
widgets: ["One"],
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["Two"],
},
},
};
async function awaitConfigChange() {
return await page.evaluate(async () => {
let resolve;
const timer = new Promise((x) => {
resolve = x;
});
workspace.addEventListener("workspace-layout-update", resolve);
await timer;
workspace.removeEventListener(
"workspace-layout-update",
resolve,
);
return await workspace.save();
});
}
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.restore(config);
await workspace.flush();
}, config);
// Apply filters for both "Category" and "Segment" via programmatic
// dispatch.
let cfgPromise = awaitConfigChange();
await page.evaluate(async () => {
const masterViewer = document.querySelector(
".workspace-master-widget",
);
masterViewer.dispatchEvent(
new CustomEvent("perspective-global-filter", {
bubbles: true,
composed: true,
detail: new PerspectiveSelectDetail(
true,
{},
["Category", "Segment"],
[],
[
{
filter: [
["Category", "==", "Furniture"],
["Segment", "==", "Consumer"],
],
},
],
),
}),
);
});
let cfg = await cfgPromise;
expect(cfg.viewers.Two.filter).toEqual([
["Category", "==", "Furniture"],
["Segment", "==", "Consumer"],
]);
// Remove only "Category"; "Segment" filter should be preserved.
cfgPromise = awaitConfigChange();
await page.evaluate(async () => {
const masterViewer = document.querySelector(
".workspace-master-widget",
);
masterViewer.dispatchEvent(
new CustomEvent("perspective-global-filter", {
bubbles: true,
composed: true,
detail: new PerspectiveSelectDetail(
true,
{},
[],
[{ filter: [["Category", "==", "Furniture"]] }],
[],
),
}),
);
});
cfg = await cfgPromise;
// Category is cleared, Segment is preserved.
expect(cfg.viewers.Two.filter).toEqual([["Segment", "==", "Consumer"]]);
});
test("Child classes of datagrid behave the same way", async ({ page }) => {
const config = {
viewers: {
One: {
table: "superstore",
name: "Test",
group_by: ["State"],
columns: ["Sales"],
plugin: "My Datagrid",
},
Two: { table: "superstore", name: "One" },
},
master: {
widgets: ["One"],
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["Two"],
},
},
};
await page.evaluate(async () => {
class MyGrid extends customElements.get(
"perspective-viewer-datagrid",
) {
get_static_config() {
return {
...super.get_static_config(),
name: "My Datagrid",
};
}
}
customElements.define("my-grid", MyGrid);
customElements.get("perspective-viewer").registerPlugin("my-grid");
});
async function awaitConfigChange() {
return await page.evaluate(async () => {
let resolve;
const timer = new Promise((x) => {
resolve = x;
});
workspace.addEventListener("workspace-layout-update", resolve);
await timer;
workspace.removeEventListener(
"workspace-layout-update",
resolve,
);
return await workspace.save();
});
}
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.restore(config);
await workspace.flush();
}, config);
let cfgPromise = awaitConfigChange();
await page
.locator(".workspace-master-widget my-grid")
.locator("tbody tr:nth-child(6) th:last-of-type")
.click();
let cfg = await cfgPromise;
expect(cfg.viewers.Two.filter).toEqual([["State", "==", "Colorado"]]);
cfgPromise = awaitConfigChange();
await page
.locator(".workspace-master-widget my-grid")
.locator("tbody tr:nth-child(6) th:last-of-type")
.click({
delay: 10,
});
cfg = await cfgPromise;
// console.log("OLD", cfg.viewers.Two.filter);
// await page.evaluate(async () => {
// await new Promise((r) => setTimeout(r, 5000));
// });
// cfg = await page.evaluate(async () => {
// const cfg = await workspace.save();
// console.log("NEW", JSON.stringify(cfg.viewers.Two.filter));
// return cfg;
// });
expect(cfg.viewers.Two.filter).toEqual([]);
return compare(page, `${context}-my-datagrid-filters-work.txt`);
});
}
test.describe("Workspace global filters", () => {
test.describe("Light DOM", () => {
tests("light-dom", compareLightDOMContents);
});
test.describe("Shadow DOM", () => {
tests("shadow-dom", compareShadowDOMContents);
});
});
+117
View File
@@ -0,0 +1,117 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 } from "@perspective-dev/test";
import {
compareLightDOMContents,
compareShadowDOMContents,
} from "@perspective-dev/test";
async function createOneWorkspace(page) {
// await new Promise((x) => setTimeout(x, 2000));
await page.evaluate(async () => {
document.querySelector("perspective-workspace").innerHTML = `
<perspective-viewer table="superstore"></perspective-viewer>
`;
const workspace = document.body.querySelector("perspective-workspace");
await workspace.load(window.__WORKER__);
await workspace.flush();
});
}
async function createMultipleViewers(page) {
await page.evaluate(async () => {
document.querySelector("perspective-workspace").innerHTML = `
<perspective-viewer table="superstore"></perspective-viewer>
<perspective-viewer table="superstore"></perspective-viewer>
`;
const workspace = document.body.querySelector("perspective-workspace");
await workspace.flush();
});
}
async function createMultipleViewersWithNames(page) {
await page.evaluate(async () => {
document.querySelector("perspective-workspace").innerHTML = `
<perspective-viewer name="Table 1" table="superstore"></perspective-viewer>
<perspective-viewer name="Table 2" table="superstore"></perspective-viewer>
`;
const workspace = document.body.querySelector("perspective-workspace");
await workspace.flush();
});
}
test.beforeEach(async ({ page }) => {
await page.goto("/tools/test/src/html/workspace-test.html");
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
});
test.describe("Workspace HTML", () => {
test.describe("Light DOM", () => {
test("Create One", async ({ page }) => {
await createOneWorkspace(page);
await compareLightDOMContents(
page,
"workspace-html-light-create-one.txt",
);
});
test("Create Multiple", async ({ page }) => {
await createMultipleViewers(page);
await compareLightDOMContents(
page,
"workspace-html-light-create-multiple.txt",
);
});
test("Create Multiple with names", async ({ page }) => {
await createMultipleViewersWithNames(page);
await compareLightDOMContents(
page,
"workspace-html-light-create-multiple-with-names.txt",
);
});
});
test.describe("Shadow DOM", () => {
test("Create One", async ({ page }) => {
await createOneWorkspace(page);
await compareShadowDOMContents(
page,
"workspace-html-shadow-create-one.txt",
);
});
test("Create Multiple", async ({ page }) => {
await createMultipleViewers(page);
await compareShadowDOMContents(
page,
"workspace-html-shadow-create-multiple.txt",
);
});
test("Create Multiple with names", async ({ page }) => {
await createMultipleViewersWithNames(page);
await compareShadowDOMContents(
page,
"workspace-html-shadow-create-multiple-with-names.txt",
);
});
});
});
+260
View File
@@ -0,0 +1,260 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 } from "@perspective-dev/test";
import {
compareLightDOMContents,
compareShadowDOMContents,
} from "@perspective-dev/test";
test.beforeEach(async ({ page }) => {
await page.goto("/tools/test/src/html/workspace-test.html");
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
});
function tests(context, compare) {
test("restore workspace with detail only", async ({ page }) => {
const config = {
viewers: {
One: { table: "superstore", name: "One" },
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["One"],
},
},
};
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.restore(config);
}, config);
await page.evaluate(async () => {
await workspace.flush();
});
return compare(
page,
`${context}-restore-workspace-with-detail-only.txt`,
);
});
test("restore workspace with master and detail", async ({ page }) => {
const config = {
viewers: {
One: {
table: "superstore",
name: "Test",
group_by: ["State"],
columns: ["Sales", "Profit"],
},
Two: { table: "superstore", name: "One" },
},
master: {
widgets: ["One"],
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["Two"],
},
},
};
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.restore(config);
}, config);
await page.evaluate(async () => {
const workspace = document.getElementById("workspace");
await workspace.flush();
});
return compare(
page,
`${context}-restore-workspace-with-master-and-detail.txt`,
);
});
test("restore workspace is symmetric with addViewer", async ({ page }) => {
const initial = await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.addViewer({ table: "superstore" });
await workspace.flush();
const result = await workspace.save();
return result;
});
const second = await page.evaluate(async (initial) => {
const workspace = document.getElementById("workspace");
await workspace.restore(initial);
await workspace.flush();
return await workspace.save();
}, initial);
expect(initial).toEqual(second);
return compare(
page,
`${context}-restore-workspace-is-symmetric-with-addviewer.txt`,
);
});
test.describe("Toggle master/detail", () => {
test("restore a blank view removes master panel", async ({ page }) => {
const config = {
detail: {
main: {
type: "tab-area",
widgets: ["PERSPECTIVE_GENERATED_ID_1"],
currentIndex: 0,
},
},
master: { widgets: ["PERSPECTIVE_GENERATED_ID_0"], sizes: [1] },
viewers: {
PERSPECTIVE_GENERATED_ID_0: {
table: "superstore",
},
PERSPECTIVE_GENERATED_ID_1: {
table: "superstore",
},
},
};
const empty_config = {
detail: {},
viewers: {},
};
// const x = await page.evaluate(async (config) => {
// const workspace = document.getElementById("workspace");
// await workspace.restore(config);
// await workspace.flush();
// return workspace.outerHTML;
// }, config);
// test.expect(x).toEqual(
// `<perspective-workspace id=\"workspace\"><perspective-viewer slot=\"PERSPECTIVE_GENERATED_ID_1\" table=\"superstore\" theme=\"Pro Light\" class=\"lm-DockPanel-widget\"><perspective-viewer-datagrid data-edit-mode=\"READ_ONLY\" style=\"position: absolute; inset: 0px; opacity: 1;\" class=\"edit-mode-allowed\"></perspective-viewer-datagrid><perspective-viewer-datagrid-toolbar slot=\"statusbar-extra\"></perspective-viewer-datagrid-toolbar></perspective-viewer><perspective-viewer slot=\"PERSPECTIVE_GENERATED_ID_0\" table=\"superstore\" theme=\"Pro Light\" class=\"workspace-master-widget lm-SplitPanel-child\" selectable=\"\"><perspective-viewer-datagrid data-edit-mode=\"READ_ONLY\" style=\"position: absolute; inset: 0px; opacity: 1;\"></perspective-viewer-datagrid><perspective-viewer-datagrid-toolbar slot=\"statusbar-extra\"></perspective-viewer-datagrid-toolbar></perspective-viewer></perspective-workspace>`,
// );
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.restore(config);
await workspace.flush();
}, empty_config);
return compare(
page,
`${context}-restore-a-blank-view-removes-master-panel.txt`,
);
});
test("restore a view which reuses a master viewer, removes master panel", async ({
page,
}) => {
const config = {
detail: {
main: {
type: "tab-area",
widgets: ["PERSPECTIVE_GENERATED_ID_1"],
currentIndex: 0,
},
},
master: { widgets: ["PERSPECTIVE_GENERATED_ID_0"], sizes: [1] },
viewers: {
PERSPECTIVE_GENERATED_ID_0: {
table: "superstore",
title: "One",
},
PERSPECTIVE_GENERATED_ID_1: {
table: "superstore",
title: "Two",
},
},
};
const config2 = {
detail: {
main: {
type: "split-area",
orientation: "horizontal",
children: [
{
type: "tab-area",
widgets: ["PERSPECTIVE_GENERATED_ID_0"],
currentIndex: 0,
},
{
type: "tab-area",
widgets: ["PERSPECTIVE_GENERATED_ID_1"],
currentIndex: 0,
},
],
sizes: [0.5, 0.5],
},
},
master: { widgets: [], sizes: [] },
viewers: {
PERSPECTIVE_GENERATED_ID_0: {
table: "superstore",
title: "One",
},
PERSPECTIVE_GENERATED_ID_1: {
table: "superstore",
title: "Two",
},
},
};
// const x = await page.evaluate(async (config) => {
// const workspace = document.getElementById("workspace");
// await workspace.restore(config);
// await workspace.flush();
// return workspace.outerHTML;
// }, config);
// test.expect(x).toEqual(
// `<perspective-workspace id="workspace"><perspective-viewer slot="PERSPECTIVE_GENERATED_ID_1" table="superstore" theme="Pro Light" class="lm-DockPanel-widget"><perspective-viewer-datagrid data-edit-mode="READ_ONLY" style="position: absolute; inset: 0px; opacity: 1;" class="edit-mode-allowed"></perspective-viewer-datagrid><perspective-viewer-datagrid-toolbar slot="statusbar-extra"></perspective-viewer-datagrid-toolbar></perspective-viewer><perspective-viewer slot="PERSPECTIVE_GENERATED_ID_0" table="superstore" theme="Pro Light" class="workspace-master-widget lm-SplitPanel-child" selectable=""><perspective-viewer-datagrid data-edit-mode="READ_ONLY" style="position: absolute; inset: 0px; opacity: 1;"></perspective-viewer-datagrid><perspective-viewer-datagrid-toolbar slot="statusbar-extra"></perspective-viewer-datagrid-toolbar></perspective-viewer></perspective-workspace>`,
// );
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.restore(config);
await workspace.flush();
}, config2);
return compare(
page,
`${context}-restore-which-reseats-master-viewer.txt`,
);
});
});
}
test.describe("Workspace restore", () => {
test.describe("Light DOM", () => {
tests("light-dom", compareLightDOMContents);
});
test.describe("Shadow DOM", () => {
tests("shadow-dom", compareShadowDOMContents);
});
});
+164
View File
@@ -0,0 +1,164 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 } from "@perspective-dev/test";
import {
compareLightDOMContents,
compareShadowDOMContents,
} from "@perspective-dev/test";
test.beforeEach(async ({ page }) => {
await page.goto("/tools/test/src/html/workspace-test.html");
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
});
function tests(context, compare) {
// TODO: 3.x `Client` has no exact `replaceTable`
test.skip("replaceTable() frees the `Table` before resolution", async ({
page,
}) => {
const config = {
viewers: {
One: { table: "superstore", name: "One" },
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["One"],
},
},
};
await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.restore(config);
await workspace.replaceTable(
"superstore",
window.__WORKER__.table("x\n1"),
);
await window.__TABLE__.delete();
}, config);
await page.evaluate(async () => {
await workspace.flush();
});
return compare(page, `${context}-replace-table-frees-table.txt`);
});
test.skip("replaceTable() works when previous table errored", async ({
page,
}) => {
const config = {
viewers: {
One: { table: "errored", name: "One" },
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["One"],
},
},
};
const result = await page.evaluate(async (config) => {
const workspace = document.getElementById("workspace");
await workspace.addTable(
"errored",
new Promise((_, reject) => setTimeout(reject, 50)),
);
try {
await workspace.restore(config);
} catch (e) {
console.error(e);
return e.toString();
}
}, config);
// NOTE This is the error message we expect when `restore()` is called
// without a `Table`, subject to change.
expect(result).toEqual(
"Error: Failed to construct table from JsValue(undefined)",
);
await page.evaluate(async () => {
await workspace.replaceTable(
"errored",
window.__WORKER__.table("x\n1"),
);
});
await page.evaluate(async () => {
await workspace.flush();
});
return compare(
page,
`${context}-replace-table-works-with-errored-table.txt`,
);
});
test("removeTable() smoke test", async ({ page }) => {
await page.evaluate(async () => {
await window.__WORKER__.table("x\n1\n", { name: "temptable" });
const workspace = document.getElementById("workspace");
await workspace.load(window.__WORKER__);
await workspace.restore({
viewers: {
One: { table: "temptable", name: "One" },
},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: ["One"],
},
},
});
await workspace.flush();
});
await page.evaluate(async () => {
await workspace.restore({
viewers: {},
detail: {
main: {
currentIndex: 0,
type: "tab-area",
widgets: [],
},
},
});
await workspace.flush();
});
return compare(page, `${context}-table-delete-works.txt`);
});
}
test.describe("Workspace table functions", () => {
test.describe("Light DOM", () => {
tests("light-dom", compareLightDOMContents);
});
test.describe("Shadow DOM", () => {
tests("shadow-dom", compareShadowDOMContents);
});
});
@@ -0,0 +1,113 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 } from "@perspective-dev/test";
import {
compareLightDOMContents,
compareShadowDOMContents,
} from "@perspective-dev/test";
const BAD_LAYOUT = {
sizes: [1],
detail: {
main: {
type: "tab-area",
widgets: [
"PERSPECTIVE_GENERATED_ID_0",
"PERSPECTIVE_GENERATED_ID_1",
],
currentIndex: 1,
},
},
mode: "globalFilters",
viewers: {
PERSPECTIVE_GENERATED_ID_0: {
plugin: "Sunburst",
plugin_config: {},
settings: false,
theme: null,
group_by: ["State"],
split_by: [],
columns: ["Quantity", null, null],
filter: [],
sort: [],
expressions: {},
aggregates: {},
master: false,
name: "one",
table: "superstore",
linked: false,
},
PERSPECTIVE_GENERATED_ID_1: {
plugin: "Sunburst",
plugin_config: {},
settings: false,
theme: null,
group_by: ["State"],
split_by: [],
columns: ["Sales", null, null],
filter: [],
sort: [],
expressions: {},
aggregates: {},
master: false,
name: "two",
table: "superstore",
linked: false,
},
},
};
function tests(context, compare) {
test.describe("visibility", () => {
test("Sunburst charts do not loop forever when disconnected from DOM", async ({
page,
}) => {
await page.evaluate(async (layout) => {
await window.workspace.restore(layout);
}, BAD_LAYOUT);
await page.evaluate(async () => {
const viewer = document.body.querySelector(
"perspective-viewer[slot=PERSPECTIVE_GENERATED_ID_0]",
);
const workspace = document.getElementById("workspace");
workspace.removeChild(viewer);
await workspace.flush();
});
return compare(
page,
`sunburst-charts-does-not-loop-${context}.txt`,
);
});
});
}
test.beforeEach(async ({ page }) => {
await page.goto("/tools/test/src/html/workspace-test.html");
await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
});
test.describe("Workspace Visibility", () => {
test.describe("Light DOM", () => {
tests("light-dom", compareLightDOMContents);
});
test.describe("Shadow DOM", () => {
tests("shadow-dom", compareShadowDOMContents);
});
});