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

625 lines
24 KiB
JavaScript

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 perspective from "./perspective_client";
const data = {
w: [1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5],
x: [1, 2, 3, 4, 4, 3, 2, 1],
y: ["a", "b", "c", "d", "a", "b", "c", "d"],
z: [true, false, true, false, true, false, true, false],
};
((perspective) => {
test.describe("group_rollup_mode", function () {
test.describe("flat", function () {
test("only emits leaves", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{ __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 },
]);
view.delete();
table.delete();
});
test("num_rows returns leaf count", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
});
const num_rows = await view.num_rows();
expect(num_rows).toEqual(4);
view.delete();
table.delete();
});
test("to_columns works", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
});
const cols = await view.to_columns();
expect(cols).toStrictEqual({
__ROW_PATH__: [["a"], ["b"], ["c"], ["d"]],
w: [7, 9, 11, 13],
x: [5, 5, 5, 5],
y: [2, 2, 2, 2],
z: [2, 2, 2, 2],
});
view.delete();
table.delete();
});
test("sort asc", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
sort: [["w", "asc"]],
});
const cols = await view.to_columns();
expect(cols).toStrictEqual({
__ROW_PATH__: [["a"], ["b"], ["c"], ["d"]],
w: [7, 9, 11, 13],
x: [5, 5, 5, 5],
y: [2, 2, 2, 2],
z: [2, 2, 2, 2],
});
view.delete();
table.delete();
});
test("sort desc", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
sort: [["w", "desc"]],
});
const cols = await view.to_columns();
expect(cols).toStrictEqual({
__ROW_PATH__: [["d"], ["c"], ["b"], ["a"]],
w: [13, 11, 9, 7],
x: [5, 5, 5, 5],
y: [2, 2, 2, 2],
z: [2, 2, 2, 2],
});
view.delete();
table.delete();
});
test("sort with hidden column", async function () {
const table = await perspective.table(data);
const view = await table.view({
columns: ["y"],
group_by: ["y"],
group_rollup_mode: "flat",
sort: [["x", "desc"]],
});
const cols = await view.to_columns();
expect(cols).toStrictEqual({
__ROW_PATH__: [["a"], ["b"], ["c"], ["d"]],
y: [2, 2, 2, 2],
});
view.delete();
table.delete();
});
test("multi-level group_by", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y", "z"],
group_rollup_mode: "flat",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{
__ROW_PATH__: ["a", true],
w: 7,
x: 5,
y: 2,
z: 2,
},
{
__ROW_PATH__: ["b", false],
w: 9,
x: 5,
y: 2,
z: 2,
},
{
__ROW_PATH__: ["c", true],
w: 11,
x: 5,
y: 2,
z: 2,
},
{
__ROW_PATH__: ["d", false],
w: 13,
x: 5,
y: 2,
z: 2,
},
]);
view.delete();
table.delete();
});
test("multi-level group_by with sort matches expanded tree order", async function () {
const table = await perspective.table(data);
const flat_view = await table.view({
group_by: ["y", "z"],
group_rollup_mode: "flat",
sort: [["w", "desc"]],
});
const flat_cols = await flat_view.to_columns();
expect(flat_cols).toStrictEqual({
__ROW_PATH__: [
["d", false],
["c", true],
["b", false],
["a", true],
],
w: [13, 11, 9, 7],
x: [5, 5, 5, 5],
y: [2, 2, 2, 2],
z: [2, 2, 2, 2],
});
flat_view.delete();
table.delete();
});
test("with split_by", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
split_by: ["z"],
group_rollup_mode: "flat",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{
__ROW_PATH__: ["a"],
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 7,
"true|x": 5,
"true|y": 2,
"true|z": 2,
},
{
__ROW_PATH__: ["b"],
"false|w": 9,
"false|x": 5,
"false|y": 2,
"false|z": 2,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
__ROW_PATH__: ["c"],
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 11,
"true|x": 5,
"true|y": 2,
"true|z": 2,
},
{
__ROW_PATH__: ["d"],
"false|w": 13,
"false|x": 5,
"false|y": 2,
"false|z": 2,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
]);
view.delete();
table.delete();
});
test("split_by only", async function () {
const table = await perspective.table(data);
const view = await table.view({
split_by: ["z"],
group_rollup_mode: "flat",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 1.5,
"true|x": 1,
"true|y": "a",
"true|z": true,
},
{
"false|w": 2.5,
"false|x": 2,
"false|y": "b",
"false|z": false,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 3.5,
"true|x": 3,
"true|y": "c",
"true|z": true,
},
{
"false|w": 4.5,
"false|x": 4,
"false|y": "d",
"false|z": false,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 5.5,
"true|x": 4,
"true|y": "a",
"true|z": true,
},
{
"false|w": 6.5,
"false|x": 3,
"false|y": "b",
"false|z": false,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 7.5,
"true|x": 2,
"true|y": "c",
"true|z": true,
},
{
"false|w": 8.5,
"false|x": 1,
"false|y": "d",
"false|z": false,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
]);
view.delete();
table.delete();
});
test("with split_by and sort", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
split_by: ["z"],
group_rollup_mode: "flat",
sort: [["w", "desc"]],
});
const json = await view.to_json();
expect(json).toStrictEqual([
{
__ROW_PATH__: ["d"],
"false|w": 13,
"false|x": 5,
"false|y": 2,
"false|z": 2,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
__ROW_PATH__: ["c"],
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 11,
"true|x": 5,
"true|y": 2,
"true|z": 2,
},
{
__ROW_PATH__: ["b"],
"false|w": 9,
"false|x": 5,
"false|y": 2,
"false|z": 2,
"true|w": null,
"true|x": null,
"true|y": null,
"true|z": null,
},
{
__ROW_PATH__: ["a"],
"false|w": null,
"false|x": null,
"false|y": null,
"false|z": null,
"true|w": 7,
"true|x": 5,
"true|y": 2,
"true|z": 2,
},
]);
view.delete();
table.delete();
});
test("updates after table.update()", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
});
const before = await view.to_json();
expect(before).toStrictEqual([
{ __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 },
]);
table.update([{ w: 9.5, x: 5, y: "e", z: true }]);
const after = await view.to_json();
expect(after).toStrictEqual([
{ __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["e"], w: 9.5, x: 5, y: 1, z: 1 },
]);
view.delete();
table.delete();
});
test("updates preserve sort order", async function () {
const table = await perspective.table(data);
const flat_view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
sort: [["w", "desc"]],
});
table.update([{ w: 100, x: 5, y: "e", z: true }]);
const flat_cols = await flat_view.to_columns();
expect(flat_cols).toStrictEqual({
__ROW_PATH__: [["e"], ["d"], ["c"], ["b"], ["a"]],
w: [100, 13, 11, 9, 7],
x: [5, 5, 5, 5, 5],
y: [1, 2, 2, 2, 2],
z: [1, 2, 2, 2, 2],
});
flat_view.delete();
table.delete();
});
test("viewport pagination", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
});
const json = await view.to_json({
start_row: 0,
end_row: 2,
});
expect(json).toStrictEqual([
{ __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 },
]);
view.delete();
table.delete();
});
test("viewport pagination with sort", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "flat",
sort: [["w", "desc"]],
});
const json = await view.to_json({
start_row: 0,
end_row: 2,
});
expect(json).toStrictEqual([
{ __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 },
{ __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 },
]);
view.delete();
table.delete();
});
});
test.describe("total", function () {
test.skip("returns only grand total with group_by", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_by: ["y"],
group_rollup_mode: "total",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{ __ROW_PATH__: [], w: 40, x: 20, y: 8, z: 4 },
]);
view.delete();
table.delete();
});
test("returns only grand total schema", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_rollup_mode: "total",
});
const json = await view.schema();
expect(json).toStrictEqual({
w: "float",
x: "integer",
y: "integer",
z: "integer",
});
view.delete();
table.delete();
});
test("returns only grand total", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_rollup_mode: "total",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{ __ROW_PATH__: [], w: 40, x: 20, y: 8, z: 8 },
]);
view.delete();
table.delete();
});
test("num_rows returns 1 without group_by", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_rollup_mode: "total",
});
const num_rows = await view.num_rows();
expect(num_rows).toEqual(1);
view.delete();
table.delete();
});
test("to_columns works", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_rollup_mode: "total",
});
const cols = await view.to_columns();
expect(cols).toStrictEqual({
w: [40],
x: [20],
y: [8],
z: [8],
});
view.delete();
table.delete();
});
test("with split_by", async function () {
const table = await perspective.table(data);
const view = await table.view({
split_by: ["z"],
group_rollup_mode: "total",
});
const json = await view.to_json();
expect(json).toStrictEqual([
{
"false|w": 22,
"false|x": 10,
"false|y": 4,
"false|z": 4,
"true|w": 18,
"true|x": 10,
"true|y": 4,
"true|z": 4,
},
]);
view.delete();
table.delete();
});
test("with split_by schema", async function () {
const table = await perspective.table(data);
const view = await table.view({
split_by: ["z"],
group_rollup_mode: "total",
});
const schema = await view.schema();
expect(schema).toStrictEqual({
w: "float",
x: "integer",
y: "integer",
z: "integer",
});
view.delete();
table.delete();
});
test("updates after table.update()", async function () {
const table = await perspective.table(data);
const view = await table.view({
group_rollup_mode: "total",
});
const before = await view.to_json();
expect(before).toStrictEqual([
{ __ROW_PATH__: [], w: 40, x: 20, y: 8, z: 8 },
]);
table.update([{ w: 10, x: 5, y: "e", z: true }]);
const after = await view.to_json();
expect(after).toStrictEqual([
{ __ROW_PATH__: [], w: 50, x: 25, y: 9, z: 9 },
]);
view.delete();
table.delete();
});
});
});
})(perspective);