From 2c0b11756e74fc104f5c40ac1ad467e94d40fefb Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 15 Mar 2026 22:35:27 -0400 Subject: [PATCH 1/3] Update UI integration test suite Signed-off-by: Andrew Stein --- .gitignore | 1 + docs/src/components/Demo/layouts.js | 7 + examples/blocks/src/dataset/index.html | 4 +- .../viewer-d3fc/test/js/axisLabel.spec.ts | 60 ++- packages/viewer-d3fc/test/js/line.spec.ts | 4 +- pnpm-lock.yaml | 71 +-- pnpm-workspace.yaml | 4 +- rust/perspective-viewer/package.json | 1 + .../test/html/column-selector-modes.html | 23 +- .../test/html/column-settings-enabled.html | 47 ++ .../js/column_settings/attributes_tab.spec.ts | 7 +- .../test/js/column_settings/datagrid.spec.ts | 13 +- .../js/column_settings/interactions.spec.ts | 4 +- .../number_string_format.spec.ts | 284 ++++++----- .../sidebar.spec.ts} | 30 +- .../test/js/column_settings/xy.spec.ts | 9 +- .../test/js/dragdrop.spec.ts | 479 ------------------ .../test/js/dragdrop/cancel.spec.ts | 200 ++++++++ .../test/js/dragdrop/dragdrop_test_utils.ts | 88 ++++ .../test/js/dragdrop/dragover.spec.ts | 189 +++++++ .../test/js/dragdrop/drop.spec.ts | 182 +++++++ .../js/dragdrop/drop_column_settings.spec.ts | 209 ++++++++ .../js/dragdrop/drop_named_columns.spec.ts | 229 +++++++++ .../perspective-viewer/test/js/errors.spec.js | 62 --- rust/perspective-viewer/test/js/helpers.ts | 129 +++++ .../test/js/helpers/standard_tests.ts | 200 ++++++++ .../test/js/render_warning_tests.js | 246 --------- .../test/js/settings.spec.js | 119 ----- .../test/js/settings_panel/close.spec.ts | 87 ++++ .../plugins.spec.ts} | 10 +- .../test/js/settings_panel/toggle.spec.ts | 59 +++ .../export.spec.ts} | 8 +- .../leaks.spec.ts} | 23 +- .../inline.spec.ts} | 58 ++- .../standard.spec.ts} | 26 +- .../delete.spec.ts} | 6 +- .../{dom.spec.js => viewer_api/dom.spec.ts} | 120 +++-- .../test/js/{ => viewer_api}/events.spec.ts | 71 +-- .../flush.spec.ts} | 10 +- .../focus.spec.ts} | 6 +- .../test/js/viewer_api/load.spec.ts | 142 ++++++ .../save_restore.spec.ts} | 46 +- .../test/js/viewer_api/theme.spec.ts | 90 ++++ .../test/js/viewer_api/view_lifecycle.spec.ts | 91 ++++ .../cancellable.spec.ts} | 27 +- .../custom_elements.spec.ts} | 25 +- .../expressions.spec.ts} | 125 ++--- .../filter.spec.ts} | 67 +-- .../localization.spec.ts} | 15 +- .../test/js/viewnotfound.spec.js | 87 ---- tools/test/playwright.config.ts | 7 +- tools/test/results.tar.gz | Bin 159829 -> 171912 bytes tools/test/src/js/models/page.ts | 4 +- tools/test/src/js/simple_viewer_tests.ts | 2 +- tools/test/src/js/utils.ts | 27 +- 55 files changed, 2491 insertions(+), 1649 deletions(-) create mode 100644 rust/perspective-viewer/test/html/column-settings-enabled.html rename rust/perspective-viewer/test/js/{column_settings.spec.ts => column_settings/sidebar.spec.ts} (93%) delete mode 100644 rust/perspective-viewer/test/js/dragdrop.spec.ts create mode 100644 rust/perspective-viewer/test/js/dragdrop/cancel.spec.ts create mode 100644 rust/perspective-viewer/test/js/dragdrop/dragdrop_test_utils.ts create mode 100644 rust/perspective-viewer/test/js/dragdrop/dragover.spec.ts create mode 100644 rust/perspective-viewer/test/js/dragdrop/drop.spec.ts create mode 100644 rust/perspective-viewer/test/js/dragdrop/drop_column_settings.spec.ts create mode 100644 rust/perspective-viewer/test/js/dragdrop/drop_named_columns.spec.ts delete mode 100644 rust/perspective-viewer/test/js/errors.spec.js create mode 100644 rust/perspective-viewer/test/js/helpers.ts create mode 100644 rust/perspective-viewer/test/js/helpers/standard_tests.ts delete mode 100644 rust/perspective-viewer/test/js/render_warning_tests.js delete mode 100644 rust/perspective-viewer/test/js/settings.spec.js create mode 100644 rust/perspective-viewer/test/js/settings_panel/close.spec.ts rename rust/perspective-viewer/test/js/{plugins.spec.js => settings_panel/plugins.spec.ts} (93%) create mode 100644 rust/perspective-viewer/test/js/settings_panel/toggle.spec.ts rename rust/perspective-viewer/test/js/{export_table.spec.ts => stability/export.spec.ts} (93%) rename rust/perspective-viewer/test/js/{leaks.spec.js => stability/leaks.spec.ts} (90%) rename rust/perspective-viewer/test/js/{test_arrows.js => superstore/inline.spec.ts} (64%) rename rust/perspective-viewer/test/js/{superstore.spec.js => superstore/standard.spec.ts} (78%) rename rust/perspective-viewer/test/js/{delete.spec.js => viewer_api/delete.spec.ts} (93%) rename rust/perspective-viewer/test/js/{dom.spec.js => viewer_api/dom.spec.ts} (73%) rename rust/perspective-viewer/test/js/{ => viewer_api}/events.spec.ts (70%) rename rust/perspective-viewer/test/js/{flush.spec.js => viewer_api/flush.spec.ts} (93%) rename rust/perspective-viewer/test/js/{focus.spec.js => viewer_api/focus.spec.ts} (93%) create mode 100644 rust/perspective-viewer/test/js/viewer_api/load.spec.ts rename rust/perspective-viewer/test/js/{save_restore.spec.js => viewer_api/save_restore.spec.ts} (85%) create mode 100644 rust/perspective-viewer/test/js/viewer_api/theme.spec.ts create mode 100644 rust/perspective-viewer/test/js/viewer_api/view_lifecycle.spec.ts rename rust/perspective-viewer/test/js/{cancellable.spec.js => viewer_config/cancellable.spec.ts} (80%) rename rust/perspective-viewer/test/js/{load.spec.js => viewer_config/custom_elements.spec.ts} (78%) rename rust/perspective-viewer/test/js/{expressions.spec.js => viewer_config/expressions.spec.ts} (76%) rename rust/perspective-viewer/test/js/{regressions.spec.js => viewer_config/filter.spec.ts} (73%) rename rust/perspective-viewer/test/js/{intl.spec.js => viewer_config/localization.spec.ts} (87%) delete mode 100644 rust/perspective-viewer/test/js/viewnotfound.spec.js diff --git a/.gitignore b/.gitignore index 5823b03643..3130ad18c3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ .clangd .DS_Store .emsdk +.pnpm-store .ipynb_checkpoints .perspectiverc .vscode/* diff --git a/docs/src/components/Demo/layouts.js b/docs/src/components/Demo/layouts.js index 4c4f82f758..c2fb3c9518 100644 --- a/docs/src/components/Demo/layouts.js +++ b/docs/src/components/Demo/layouts.js @@ -31,6 +31,7 @@ export const LAYOUTS = { fg_gradient: 17.4, }, }, + group_rollup_mode: "rollup", settings: true, title: "Market Monitor", group_by: ["name"], @@ -52,6 +53,7 @@ export const LAYOUTS = { plugin: "datagrid", title: "Blotter", columns: ["ask", "bid", "chg"], + group_rollup_mode: "rollup", sort: [ ["name", "desc"], ["lastUpdate", "desc"], @@ -63,6 +65,7 @@ export const LAYOUTS = { }, "x bar": { title: "Px (Δ)", + group_rollup_mode: "flat", columns: ["chg"], plugin: "X Bar", sort: [["chg", "asc"]], @@ -71,6 +74,7 @@ export const LAYOUTS = { }, "y line": { title: "Time Series (Px)", + group_rollup_mode: "flat", plugin: "Y Line", group_by: ["lastUpdate"], split_by: [], @@ -81,6 +85,7 @@ export const LAYOUTS = { }, "xy scatter": { title: "Spread Scatter", + group_rollup_mode: "flat", plugin: "X/Y Scatter", group_by: ["name"], split_by: [], @@ -90,6 +95,7 @@ export const LAYOUTS = { }, treemap: { plugin: "Treemap", + group_rollup_mode: "flat", title: "Volume Map", group_by: ["name", "client"], split_by: [], @@ -101,6 +107,7 @@ export const LAYOUTS = { ], }, heatmap: { + group_rollup_mode: "flat", title: "Spread Heatmap", columns: ["name"], plugin: "Heatmap", diff --git a/examples/blocks/src/dataset/index.html b/examples/blocks/src/dataset/index.html index 720a92da1b..11c949f92f 100644 --- a/examples/blocks/src/dataset/index.html +++ b/examples/blocks/src/dataset/index.html @@ -175,13 +175,13 @@ const make_run_click_callback = (state) => async () => { state.table?.delete?.({ lazy: true }); state.table = gen_data(); - await window.psp_workspace.addTable("superstore", state.table); + // await window.psp_workspace.addTable("superstore", state.table); }; const make_del_click_callback = (state) => async () => { if (state.table) { // await viewer.eject(); - await window.psp_workspace.removeTable("superstore"); + // await window.psp_workspace.removeTable("superstore"); await state.table.then((x) => x.delete({ lazy: true })); state.table = undefined; } diff --git a/packages/viewer-d3fc/test/js/axisLabel.spec.ts b/packages/viewer-d3fc/test/js/axisLabel.spec.ts index 849d425dab..050e378c72 100644 --- a/packages/viewer-d3fc/test/js/axisLabel.spec.ts +++ b/packages/viewer-d3fc/test/js/axisLabel.spec.ts @@ -60,7 +60,7 @@ function confirmDataIsNotEpochForm(dateValues: any[]) { test.describe("Axis Values With Grouped Data With A Date Field In The Group", () => { test("X Bar y-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="X Bar"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-xbar"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -93,7 +93,7 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("Y Bar x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Y Bar"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-ybar"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -126,7 +126,7 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("OHLC x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="OHLC"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-ohlc"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -159,40 +159,44 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("Heatmap x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Heatmap"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-heatmap"); - const dateValues = await page.evaluate(async () => { - let viewer = document.querySelector("perspective-viewer"); + const dateValues = await page + .evaluate(async () => { + let viewer = document.querySelector("perspective-viewer"); - if (!viewer) { - return Error("Invalid Viewer"); - } + if (!viewer) { + return Error("Invalid Viewer"); + } - const plugin_element = viewer.querySelector( - `perspective-viewer-d3fc-heatmap`, - ); + const plugin_element = viewer.querySelector( + `perspective-viewer-d3fc-heatmap`, + ); - if (!plugin_element) { - throw Error("Invalid Plugin Element"); - } + if (!plugin_element) { + throw Error("Invalid Plugin Element"); + } - const shadowRoot = plugin_element.shadowRoot; - const dateTextElements = shadowRoot.querySelectorAll( - "div d3fc-group d3fc-svg.x-axis.bottom-axis svg g.group:last-child g.tick text", - ); + const shadowRoot = plugin_element.shadowRoot; + const dateTextElements = shadowRoot.querySelectorAll( + "div d3fc-group d3fc-svg.x-axis.bottom-axis svg g.group:last-child g.tick text", + ); - // collect and return the actual date data to be used. - return Array.from(dateTextElements).map((el) => - el.textContent?.trim(), - ); - }); + // collect and return the actual date data to be used. + return Array.from(dateTextElements).map((el) => + el.textContent?.trim(), + ); + }) + .catch((e) => e); + + await page.pause(); confirmDataIsNotEpochForm(dateValues); }); test("Y Line x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Y Line"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-yline"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -225,7 +229,7 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("Y Area x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Y Area"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-yarea"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -258,7 +262,7 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("Y Scatter x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Y Scatter"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-yscatter"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -291,7 +295,7 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("CandleStick x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Candlestick"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-candlestick"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); diff --git a/packages/viewer-d3fc/test/js/line.spec.ts b/packages/viewer-d3fc/test/js/line.spec.ts index 7258a4e16b..1023b60762 100644 --- a/packages/viewer-d3fc/test/js/line.spec.ts +++ b/packages/viewer-d3fc/test/js/line.spec.ts @@ -72,9 +72,7 @@ test.describe("Line regressions", () => { ?.shadowRoot?.innerHTML; }); - compareContentsToSnapshot(out!, [ - "line-charts-denser-than-one-second-regression.txt", - ]); + await compareContentsToSnapshot(out!); }); test("Zoom on a chart with split Y axis renders the right axis", async ({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6682d7e94..a1f72d589f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,11 +25,11 @@ catalogs: specifier: 3.0.0 version: 3.0.0 '@playwright/experimental-ct-react': - specifier: '=1.52.0' - version: 1.52.0 + specifier: '=1.58.0' + version: 1.58.0 '@playwright/test': - specifier: '=1.52.0' - version: 1.52.0 + specifier: '=1.58.0' + version: 1.58.0 '@prospective.co/procss': specifier: 0.1.18 version: 0.1.18 @@ -246,10 +246,10 @@ importers: version: link:packages/workspace '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) '@playwright/test': specifier: 'catalog:' - version: 1.52.0 + version: 1.58.0 chalk: specifier: 'catalog:' version: 5.6.2 @@ -1050,10 +1050,10 @@ importers: version: link:../../tools/test '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) '@playwright/test': specifier: 'catalog:' - version: 1.52.0 + version: 1.58.0 '@types/node': specifier: 'catalog:' version: 24.9.1 @@ -1142,13 +1142,16 @@ importers: version: link:../../tools/test '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) '@playwright/test': specifier: 'catalog:' - version: 1.52.0 + version: 1.58.0 '@prospective.co/procss': specifier: 'catalog:' version: 0.1.18 + '@types/node': + specifier: 'catalog:' + version: 24.9.1 '@types/react': specifier: 'catalog:' version: 19.2.2 @@ -1266,10 +1269,10 @@ importers: version: 3.0.0 '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) '@playwright/test': specifier: 'catalog:' - version: 1.52.0 + version: 1.58.0 auto-changelog: specifier: 'catalog:' version: 2.5.0 @@ -1299,10 +1302,10 @@ importers: dependencies: '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) '@playwright/test': specifier: 'catalog:' - version: 1.52.0 + version: 1.58.0 prettier: specifier: 'catalog:' version: 3.6.2 @@ -3331,17 +3334,17 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@playwright/experimental-ct-core@1.52.0': - resolution: {integrity: sha512-DiDEammXxt8OIFDfoNitoOZyHFJAu6aYi0abmHl0IZgOQHxccP6UX50aTEnSTTUWCfwUWB0Vd8TKJ6w122WJEw==} + '@playwright/experimental-ct-core@1.58.0': + resolution: {integrity: sha512-YZsjApZmRE78Kp2E6OtAvFFVheUyZDfrlZMf+lfnSshmYHrrJUy3bhdCe7EPCWsE12XfCVVAv6G0btiyAx8d0w==} engines: {node: '>=18'} - '@playwright/experimental-ct-react@1.52.0': - resolution: {integrity: sha512-r9gREinfeCAgnMp2Kpr6MnXSnKE06HlM0qWkortrtOHhD1xdGAT+mBBBP0YvPN2f169wGNIRuSOxp05MFZ+XaQ==} + '@playwright/experimental-ct-react@1.58.0': + resolution: {integrity: sha512-hm3Vddy1zNrTFfh2qwjuKvz8lY9oJm2iQkSITSMat4tztK5KxfwJrRyLGeZNCuzAy3bCWGk80Rb0ZfQ3Vitw+A==} engines: {node: '>=18'} hasBin: true - '@playwright/test@1.52.0': - resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==} + '@playwright/test@1.58.0': + resolution: {integrity: sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==} engines: {node: '>=18'} hasBin: true @@ -7224,13 +7227,13 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} - playwright-core@1.52.0: - resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} + playwright-core@1.58.0: + resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==} engines: {node: '>=18'} hasBin: true - playwright@1.52.0: - resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==} + playwright@1.58.0: + resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==} engines: {node: '>=18'} hasBin: true @@ -12470,10 +12473,10 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@playwright/experimental-ct-core@1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': + '@playwright/experimental-ct-core@1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': dependencies: - playwright: 1.52.0 - playwright-core: 1.52.0 + playwright: 1.58.0 + playwright-core: 1.58.0 vite: 6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' @@ -12488,9 +12491,9 @@ snapshots: - tsx - yaml - '@playwright/experimental-ct-react@1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1)': + '@playwright/experimental-ct-react@1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1)': dependencies: - '@playwright/experimental-ct-core': 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@playwright/experimental-ct-core': 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) transitivePeerDependencies: - '@types/node' @@ -12507,9 +12510,9 @@ snapshots: - vite - yaml - '@playwright/test@1.52.0': + '@playwright/test@1.58.0': dependencies: - playwright: 1.52.0 + playwright: 1.58.0 '@pnpm/config.env-replace@1.1.0': {} @@ -17048,11 +17051,11 @@ snapshots: dependencies: find-up: 6.3.0 - playwright-core@1.52.0: {} + playwright-core@1.58.0: {} - playwright@1.52.0: + playwright@1.58.0: dependencies: - playwright-core: 1.52.0 + playwright-core: 1.58.0 optionalDependencies: fsevents: 2.3.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cf3f70e6bd..d9a1b37ddf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -50,8 +50,8 @@ catalog: "@fontsource/roboto-mono": "4.5.10" "@iarna/toml": "3.0.0" "@jupyterlab/builder": "^4" - "@playwright/experimental-ct-react": "=1.52.0" - "@playwright/test": "=1.52.0" + "@playwright/experimental-ct-react": "=1.58.0" + "@playwright/test": "=1.58.0" "@prospective.co/procss": "0.1.18" "@types/d3": "^7.4.3" "@types/lodash": "^4.17.20" diff --git a/rust/perspective-viewer/package.json b/rust/perspective-viewer/package.json index 8c6c40b307..a4171b688f 100644 --- a/rust/perspective-viewer/package.json +++ b/rust/perspective-viewer/package.json @@ -47,6 +47,7 @@ "@playwright/test": "catalog:", "@playwright/experimental-ct-react": "catalog:", "@types/react": "catalog:", + "@types/node": "catalog:", "@types/react-dom": "catalog:", "prettier": "catalog:", "typedoc": "catalog:", diff --git a/rust/perspective-viewer/test/html/column-selector-modes.html b/rust/perspective-viewer/test/html/column-selector-modes.html index 6df570bcc7..b15a64da16 100644 --- a/rust/perspective-viewer/test/html/column-selector-modes.html +++ b/rust/perspective-viewer/test/html/column-selector-modes.html @@ -1,7 +1,6 @@ - @@ -9,6 +8,7 @@ diff --git a/rust/perspective-viewer/test/html/column-settings-enabled.html b/rust/perspective-viewer/test/html/column-settings-enabled.html new file mode 100644 index 0000000000..04da1d5cdf --- /dev/null +++ b/rust/perspective-viewer/test/html/column-settings-enabled.html @@ -0,0 +1,47 @@ + + + + + + + + + + + diff --git a/rust/perspective-viewer/test/js/column_settings/attributes_tab.spec.ts b/rust/perspective-viewer/test/js/column_settings/attributes_tab.spec.ts index 2ef49a5835..3f387b933e 100644 --- a/rust/perspective-viewer/test/js/column_settings/attributes_tab.spec.ts +++ b/rust/perspective-viewer/test/js/column_settings/attributes_tab.spec.ts @@ -10,8 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { test, expect } from "@playwright/test"; -import { PageView } from "@perspective-dev/test"; +import { test, expect, PageView } from "../helpers.ts"; test.beforeEach(async ({ page }) => { await page.goto("/tools/test/src/html/basic-test.html"); @@ -96,6 +95,7 @@ test.describe("Attributes Tab", () => { view.columnSettingsSidebar.attributesTab.saveBtn, ).toBeDisabled(); }); + test("Tab Button Click enters 4 spaces.", async ({ page }) => { let view = new PageView(page); await view.restore({ @@ -128,6 +128,7 @@ test.describe("Attributes Tab", () => { ); expect(caretPosition).toBe(4); // length of foo + length of '\t' = 4; }); + test("Reset button", async ({ page }) => { let view = new PageView(page); await view.restore({ @@ -162,6 +163,7 @@ test.describe("Attributes Tab", () => { "12345", ); }); + test("Delete button", async ({ page }) => { let view = new PageView(page); await view.restore({ @@ -186,6 +188,7 @@ test.describe("Attributes Tab", () => { let config = await view.save(); expect(config?.expressions).toStrictEqual({}); }); + test("Rename empty header as expression value", async ({ page }) => { let view = new PageView(page); let settingsPanel = await view.openSettingsPanel(); diff --git a/rust/perspective-viewer/test/js/column_settings/datagrid.spec.ts b/rust/perspective-viewer/test/js/column_settings/datagrid.spec.ts index 2951b67850..73aa6229fb 100644 --- a/rust/perspective-viewer/test/js/column_settings/datagrid.spec.ts +++ b/rust/perspective-viewer/test/js/column_settings/datagrid.spec.ts @@ -10,11 +10,14 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { PageView as PspViewer, compareNodes } from "@perspective-dev/test"; - -import { expect, test } from "@perspective-dev/test"; - -test.describe("Regressions", function () { +import { + PageView as PspViewer, + compareNodes, + expect, + test, +} from "../helpers.ts"; + +test.describe("Datagrid Column Styles", function () { test.beforeEach(async ({ page }) => { await page.goto("/tools/test/src/html/basic-test.html"); await page.evaluate(async () => { diff --git a/rust/perspective-viewer/test/js/column_settings/interactions.spec.ts b/rust/perspective-viewer/test/js/column_settings/interactions.spec.ts index b13973cb16..31f0c16942 100644 --- a/rust/perspective-viewer/test/js/column_settings/interactions.spec.ts +++ b/rust/perspective-viewer/test/js/column_settings/interactions.spec.ts @@ -10,9 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { PageView } from "@perspective-dev/test"; -import { ColumnSelector } from "@perspective-dev/test/src/js/models/settings_panel"; -import { test, expect } from "@perspective-dev/test"; +import { PageView, ColumnSelector, test, expect } from "../helpers.ts"; test.describe.configure({ mode: "parallel" }); diff --git a/rust/perspective-viewer/test/js/column_settings/number_string_format.spec.ts b/rust/perspective-viewer/test/js/column_settings/number_string_format.spec.ts index e50667c14d..b941a58905 100644 --- a/rust/perspective-viewer/test/js/column_settings/number_string_format.spec.ts +++ b/rust/perspective-viewer/test/js/column_settings/number_string_format.spec.ts @@ -10,8 +10,13 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { PageView, compareContentsToSnapshot } from "@perspective-dev/test"; -import { test, expect } from "@perspective-dev/test"; +import { + PageView, + compareContentsToSnapshot, + test, + expect, +} from "../helpers.ts"; + import { DataGrid } from "@perspective-dev/test/src/js/models/plugins/datagrid.ts"; test.beforeEach(async ({ page }) => { @@ -23,176 +28,177 @@ test.beforeEach(async ({ page }) => { }); }); -test("Integer/float styles", async ({ page }) => { - const view = new PageView(page); - await view.restore({ - settings: true, - plugin: "Datagrid", - columns: ["Profit", "Row ID"], +test.describe("Number & String Format", () => { + test("Integer/float styles", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + plugin: "Datagrid", + columns: ["Profit", "Row ID"], + }); + const profit = + await view.settingsPanel.activeColumns.getColumnByName("Profit"); + await profit.editBtn.click(); + const styleContainer = view.columnSettingsSidebar.styleTab.container; + await styleContainer.locator("#fractional-digits-label").waitFor(); + const rowId = + await view.settingsPanel.activeColumns.getColumnByName("Row ID"); + await rowId.editBtn.click(); + await styleContainer + .getByText("Fractional Digits") + .waitFor({ state: "detached" }); }); - const profit = - await view.settingsPanel.activeColumns.getColumnByName("Profit"); - await profit.editBtn.click(); - const styleContainer = view.columnSettingsSidebar.styleTab.container; - await styleContainer.locator("#fractional-digits-label").waitFor(); - const rowId = - await view.settingsPanel.activeColumns.getColumnByName("Row ID"); - await rowId.editBtn.click(); - await styleContainer - .getByText("Fractional Digits") - .waitFor({ state: "detached" }); -}); -for (const name of ["Significant Digits", "Fractional Digits"]) { - test.skip(`Rounding Increment doesn't send when ${name} is open`, async ({ + for (const name of ["Significant Digits", "Fractional Digits"]) { + test.skip(`Rounding Increment doesn't send when ${name} is open`, async ({ + page, + }) => { + let view = new PageView(page); + await view.restore({ + settings: true, + plugin: "Datagrid", + columns: ["Profit"], + }); + const profit = + await view.settingsPanel.activeColumns.getColumnByName( + "Profit", + ); + await profit.editBtn.click(); + const styleContainer = + view.columnSettingsSidebar.styleTab.container; + await styleContainer + .locator('div[data-value="Auto"] select') + .first() + .selectOption("20"); + + await styleContainer.getByText(name).click(); + let config = await view.save(); + expect(config).toMatchObject({ + columns_config: {}, + }); + + await styleContainer.getByText(name).click(); + config = await view.save(); + expect(config).toMatchObject({ + columns_config: { + Profit: { + number_format: { + maximumFractionDigits: 0, + roundingIncrement: 20, + }, + }, + }, + }); + }); + } + + test.skip("Rounding Priority doesn't send unless Fractional and Significant Digits are open", async ({ page, }) => { - let view = new PageView(page); + const view = new PageView(page); await view.restore({ settings: true, plugin: "Datagrid", columns: ["Profit"], }); - const profit = + const col = await view.settingsPanel.activeColumns.getColumnByName("Profit"); - await profit.editBtn.click(); + await col.editBtn.click(); const styleContainer = view.columnSettingsSidebar.styleTab.container; - await styleContainer - .locator('div[data-value="Auto"] select') - .first() - .selectOption("20"); + await expect( + styleContainer.locator("#Rounding-Priority-checkbox"), + ).toBeEnabled(); - await styleContainer.getByText(name).click(); - let config = await view.save(); + const select = styleContainer + .locator('div[data-value="Auto"] select') + .nth(1); + await select.scrollIntoViewIfNeeded(); + await select.selectOption("MorePrecision"); + const config = await view.save(); expect(config).toMatchObject({ + columns_config: { + Profit: { + number_format: { + roundingPriority: "morePrecision", + }, + }, + }, + }); + await styleContainer.getByText("Fractional Digits").click(); + const config2 = await view.save(); + expect(config2).toMatchObject({ columns_config: {}, }); + }); - await styleContainer.getByText(name).click(); - config = await view.save(); - expect(config).toMatchObject({ + test("Datagrid integration", async ({ page }) => { + const view = new PageView(page); + const datagrid = new DataGrid(page); + await view.restore({ + plugin: "Datagrid", + columns: ["Profit"], columns_config: { Profit: { number_format: { + minimumIntegerDigits: 3, maximumFractionDigits: 0, - roundingIncrement: 20, + roundingIncrement: 50, + roundingMode: "ceil", + notation: "compact", + compactDisplay: "short", + signDisplay: "always", }, }, }, }); - }); -} -test.skip("Rounding Priority doesn't send unless Fractional and Significant Digits are open", async ({ - page, -}) => { - const view = new PageView(page); - await view.restore({ - settings: true, - plugin: "Datagrid", - columns: ["Profit"], - }); - const col = - await view.settingsPanel.activeColumns.getColumnByName("Profit"); - await col.editBtn.click(); - const styleContainer = view.columnSettingsSidebar.styleTab.container; - await expect( - styleContainer.locator("#Rounding-Priority-checkbox"), - ).toBeEnabled(); - - const select = styleContainer - .locator('div[data-value="Auto"] select') - .nth(1); - await select.scrollIntoViewIfNeeded(); - await select.selectOption("MorePrecision"); - const config = await view.save(); - expect(config).toMatchObject({ - columns_config: { - Profit: { - number_format: { - roundingPriority: "morePrecision", + const decimal = await datagrid.regularTable.table.innerHTML(); + await page.pause(); + await compareContentsToSnapshot(decimal, ["decimal"]); + await view.restore({ + plugin: "Datagrid", + columns: ["Profit"], + columns_config: { + Profit: { + number_format: { + style: "currency", + currency: "USD", + currencySign: "accounting", + }, }, }, - }, - }); - await styleContainer.getByText("Fractional Digits").click(); - const config2 = await view.save(); - expect(config2).toMatchObject({ - columns_config: {}, - }); -}); + }); -test("Datagrid integration", async ({ page }) => { - const view = new PageView(page); - const datagrid = new DataGrid(page); - await view.restore({ - plugin: "Datagrid", - columns: ["Profit"], - columns_config: { - Profit: { - number_format: { - minimumIntegerDigits: 3, - maximumFractionDigits: 0, - roundingIncrement: 50, - roundingMode: "ceil", - notation: "compact", - compactDisplay: "short", - signDisplay: "always", - }, - }, - }, - }); - const decimal = await datagrid.regularTable.table.innerHTML(); - await page.pause(); - await compareContentsToSnapshot(decimal, [ - "datagrid-integration-decimal.html", - ]); - await view.restore({ - plugin: "Datagrid", - columns: ["Profit"], - columns_config: { - Profit: { - number_format: { - style: "currency", - currency: "USD", - currencySign: "accounting", + const currency = await datagrid.regularTable.table.innerHTML(); + await compareContentsToSnapshot(currency, ["currency"]); + await view.restore({ + plugin: "Datagrid", + columns: ["Profit"], + columns_config: { + Profit: { + number_format: { + style: "unit", + unit: "byte", + }, }, }, - }, - }); - const currency = await datagrid.regularTable.table.innerHTML(); - await compareContentsToSnapshot(currency, [ - "datagrid-integration-currency.html", - ]); + }); - await view.restore({ - plugin: "Datagrid", - columns: ["Profit"], - columns_config: { - Profit: { - number_format: { - style: "unit", - unit: "byte", + const unit = await datagrid.regularTable.table.innerHTML(); + await compareContentsToSnapshot(unit, ["unit"]); + await view.restore({ + plugin: "Datagrid", + columns: ["Profit"], + columns_config: { + Profit: { + number_format: { + style: "percent", + }, }, }, - }, - }); - const unit = await datagrid.regularTable.table.innerHTML(); - await compareContentsToSnapshot(unit, ["datagrid-integration-unit.html"]); + }); - await view.restore({ - plugin: "Datagrid", - columns: ["Profit"], - columns_config: { - Profit: { - number_format: { - style: "percent", - }, - }, - }, + const data = await datagrid.regularTable.table.innerHTML(); + await compareContentsToSnapshot(data, ["raw"]); }); - const data = await datagrid.regularTable.table.innerHTML(); - await compareContentsToSnapshot(data, [ - "datagrid-integration-percent.html", - ]); }); diff --git a/rust/perspective-viewer/test/js/column_settings.spec.ts b/rust/perspective-viewer/test/js/column_settings/sidebar.spec.ts similarity index 93% rename from rust/perspective-viewer/test/js/column_settings.spec.ts rename to rust/perspective-viewer/test/js/column_settings/sidebar.spec.ts index c80b125e58..f4d2c3d9f8 100644 --- a/rust/perspective-viewer/test/js/column_settings.spec.ts +++ b/rust/perspective-viewer/test/js/column_settings/sidebar.spec.ts @@ -10,9 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { test, expect } from "@perspective-dev/test"; -import { PageView } from "@perspective-dev/test"; -import { ColumnSettingsSidebar } from "@perspective-dev/test/src/js/models/column_settings"; +import { test, expect, PageView, ColumnSettingsSidebar } from "../helpers.ts"; test.beforeEach(async ({ page }) => { await page.goto("/tools/test/src/html/basic-test.html"); @@ -56,8 +54,10 @@ export async function checkTab( } } -test.describe("Plugin Styles", () => { - test("Active column edit buttons open sidebar", async ({ page }) => { +test.describe("Column Settings Sidebar", () => { + test("edit button > opens sidebar for active table and expression columns", async ({ + page, + }) => { let view = new PageView(page); let settingsPanel = await view.openSettingsPanel(); let inactiveColumns = settingsPanel.inactiveColumns; @@ -79,7 +79,9 @@ test.describe("Plugin Styles", () => { await exprCol.editBtn.click(); await checkTab(view.columnSettingsSidebar, true, true, false); }); - test("Inactive column edit buttons open sidebar", async ({ page }) => { + test("edit button > opens sidebar for inactive expression columns", async ({ + page, + }) => { let view = new PageView(page); let settingsPanel = await view.openSettingsPanel(); let inactiveColumns = settingsPanel.inactiveColumns; @@ -99,7 +101,9 @@ test.describe("Plugin Styles", () => { await exprCol.editBtn.click(); await checkTab(view.columnSettingsSidebar, false, true); }); - test("Click to change tabs", async ({ page }) => { + test("tabs > switches between style and attributes tabs", async ({ + page, + }) => { let view = new PageView(page); let settingsPanel = await view.openSettingsPanel(); let sidebar = view.columnSettingsSidebar; @@ -120,7 +124,9 @@ test.describe("Plugin Styles", () => { await sidebar.styleTab.container.waitFor(); }); - test("View updates don't re-render sidebar", async ({ page }) => { + test("state > persists sidebar when table data updates", async ({ + page, + }) => { await page.evaluate(async () => { // @ts-ignore let table = await window.__TEST_WORKER__.table({ x: [0] }); @@ -151,7 +157,9 @@ test.describe("Plugin Styles", () => { await expect(view.columnSettingsSidebar.container).toBeVisible(); }); - test("Column settings should not expand", async ({ page }) => { + test("layout > constrains width with long expression text", async ({ + page, + }) => { let view = new PageView(page); const MAX_WIDTH = 350; @@ -177,7 +185,7 @@ test.describe("Plugin Styles", () => { await checkWidth(); }); - test("Selected tab stays selected when manipulating column", async ({ + test("tabs > preserves selected tab when toggling and saving expression", async ({ page, }) => { const view = new PageView(page); @@ -211,7 +219,7 @@ test.describe("Plugin Styles", () => { expect(await selectedTab()).toBe("Attributes"); }); - test("Color range component updates when switching columns to edit different number column without closing sidebar", async ({ + test("color range > resets to default when switching to a different number column", async ({ page, }) => { const view = new PageView(page); diff --git a/rust/perspective-viewer/test/js/column_settings/xy.spec.ts b/rust/perspective-viewer/test/js/column_settings/xy.spec.ts index 2ecd9b974c..8a8c3a96d4 100644 --- a/rust/perspective-viewer/test/js/column_settings/xy.spec.ts +++ b/rust/perspective-viewer/test/js/column_settings/xy.spec.ts @@ -10,10 +10,13 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { PageView as PspViewer, compareNodes } from "@perspective-dev/test"; +import { + PageView as PspViewer, + compareNodes, + expect, + test, +} from "../helpers.ts"; import { SymbolPair } from "@perspective-dev/test/src/js/models/column_settings"; - -import { expect, test } from "@perspective-dev/test"; import { Page } from "@playwright/test"; const symbols = [ diff --git a/rust/perspective-viewer/test/js/dragdrop.spec.ts b/rust/perspective-viewer/test/js/dragdrop.spec.ts deleted file mode 100644 index 18bc0aad15..0000000000 --- a/rust/perspective-viewer/test/js/dragdrop.spec.ts +++ /dev/null @@ -1,479 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ 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 { -// -// compareSVGContentsToSnapshot, -// getSvgContentString, -// SUPERSTORE_CSV_PATH, -// } from "@perspective-dev/test"; -// import path from "path"; - -// async function get_contents(page) { -// return await page.evaluate(async () => { -// // @ts-ignore -// const viewer = document -// .querySelector("perspective-viewer") -// .shadowRoot.querySelector("#app_panel"); -// return viewer ? viewer.innerHTML : "MISSING"; -// }); -// } - -// async function restore_viewer(page, config) { -// await page.evaluate(async (config) => { -// const viewer = document.querySelector("perspective-viewer"); -// // @ts-ignore -// await viewer.getTable(); -// // @ts-ignore -// await viewer.restore(config); -// }, config); -// } - -// async function shadow_elem(page, selector) { -// return await page.evaluateHandle(async (selector) => { -// const viewer = document.querySelector("perspective-viewer"); -// // @ts-ignore -// return viewer.shadowRoot.querySelector(selector); -// }, selector); -// } - -// async function drag_and_drop(page, origin, target, skip = false) { -// page.setDragInterception(true); -// if (!skip) { -// await page.evaluate(async () => { -// const viewer = document.querySelector("perspective-viewer"); -// // @ts-ignore -// window._dragdrop_finished = false; -// // @ts-ignore -// viewer.addEventListener("perspective-config-update", () => { -// // @ts-ignore -// window._dragdrop_finished = true; -// }); -// }); -// } -// await origin.dragAndDrop(target); -// if (!skip) { -// // @ts-ignore -// await page.waitForFunction(() => window._dragdrop_finished); -// } else { -// await page.waitFor(300); -// } - -// await page.evaluate(async () => { -// // @ts-ignore -// window._dragdrop_finished = false; -// const viewer = document.querySelector("perspective-viewer"); -// // @ts-ignore -// await viewer.flush(); -// }); -// } - -// async function drag(page, origin, target) { -// page.setDragInterception(true); -// origin.drop(); -// await page.waitFor(100); -// origin.dragAndDrop(target, { delay: 100000 }); -// await page.waitFor(100); -// } - -test.describe("Drag and drop", () => { - test("Drag and Drop tests file is being reach, but all tests are currently skipped", async ({ - page, - }) => { - expect(1).toBe(1); - }); -}); - -// utils.with_server({}, () => { -// describe("dragdrop", () => { -// describe.page( -// "superstore.html", -// () => { -// describe("drop", () => { -// test.skip("from inactive to active should add", async (page) => { -// await restore_viewer(page, { -// settings: true, -// group_by: ["State"], -// columns: ["Profit", "Sales"], -// }); - -// const origin = await shadow_elem( -// page, -// `#sub-columns [data-index="3"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="1"]` -// ); - -// await drag_and_drop(page, origin, target); -// return await get_contents(page); -// }); - -// test.skip("from active to active should swap", async (page) => { -// await restore_viewer(page, { -// settings: true, -// group_by: ["State"], -// columns: ["Profit", "Sales"], -// }); - -// const origin = await shadow_elem( -// page, -// `#active-columns [data-index="0"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="1"]` -// ); - -// await drag_and_drop(page, origin, target); -// return await get_contents(page); -// }); -// }); -// }, -// { root: path.join(__dirname, "..", "..") } -// ); - -// describe.page( -// "column-selector-modes.html", -// () => { -// describe("drop", () => { -// test.skip("from inactive to required column should add", async (page) => { -// await restore_viewer(page, { -// settings: true, -// plugin: "test chart", -// group_by: ["State"], -// columns: ["Profit", "Sales"], -// }); - -// const origin = await shadow_elem( -// page, -// `#sub-columns [data-index="1"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="1"]` -// ); - -// await drag_and_drop(page, origin, target); -// return await get_contents(page); -// }); - -// test.skip("from required to required should swap", async (page) => { -// await restore_viewer(page, { -// settings: true, -// group_by: ["State"], -// columns: [ -// "Profit", -// "Sales", -// null, -// "Quantity", -// "Discount", -// ], -// }); - -// const origin = await shadow_elem( -// page, -// `#active-columns [data-index="0"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="1"]` -// ); - -// await drag_and_drop(page, origin, target); -// return await get_contents(page); -// }); - -// test.skip("from required to empty column should fail", async (page) => { -// await restore_viewer(page, { -// settings: true, -// group_by: ["State"], -// columns: ["Profit", "Sales"], -// }); - -// const origin = await shadow_elem( -// page, -// `#active-columns [data-index="0"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="3"]` -// ); - -// await drag_and_drop(page, origin, target, true); -// return await get_contents(page); -// }); - -// test.skip("from inactive to empty should add", async (page) => { -// await restore_viewer(page, { -// settings: true, -// plugin: "test chart", -// group_by: ["State"], -// columns: ["Profit", "Sales"], -// }); - -// const origin = await shadow_elem( -// page, -// `#sub-columns [data-index="1"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="3"]` -// ); - -// await drag_and_drop(page, origin, target); -// return await get_contents(page); -// }); - -// test.skip("from named to required should swap", async (page) => { -// await restore_viewer(page, { -// settings: true, -// group_by: ["State"], -// columns: [ -// "Profit", -// "Sales", -// null, -// "Quantity", -// "Discount", -// ], -// }); - -// const origin = await shadow_elem( -// page, -// `#active-columns [data-index="3"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="1"]` -// ); - -// await drag_and_drop(page, origin, target); -// return await get_contents(page); -// }); - -// test.skip("from optional to empty columns should move", async (page) => { -// await restore_viewer(page, { -// settings: true, -// group_by: ["State"], -// columns: [ -// "Profit", -// "Sales", -// null, -// null, -// "Quantity", -// "Discount", -// "Category", -// ], -// }); - -// const origin = await shadow_elem( -// page, -// `#active-columns [data-index="4"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="2"]` -// ); - -// await drag_and_drop(page, origin, target); -// return await get_contents(page); -// }); -// }); - -// describe("dragover", () => { -// test.skip( -// "from named to required columns should swap", -// async (page) => { -// await restore_viewer(page, { -// settings: true, -// group_by: ["State"], -// columns: [ -// "Profit", -// "Sales", -// null, -// "Quantity", -// "Discount", -// ], -// }); - -// const origin = await shadow_elem( -// page, -// `#active-columns [data-index="3"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="1"]` -// ); - -// await drag(page, origin, target); -// return await get_contents(page); -// }, -// { reload_page: true } -// ); - -// test.skip( -// "from optional to empty columns should move", -// async (page) => { -// await restore_viewer(page, { -// settings: true, -// group_by: ["State"], -// columns: [ -// "Profit", -// "Sales", -// null, -// null, -// "Quantity", -// "Discount", -// "Category", -// ], -// }); - -// const origin = await shadow_elem( -// page, -// `#active-columns [data-index="5"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="2"]` -// ); - -// await drag(page, origin, target); -// return await get_contents(page); -// }, -// { reload_page: true } -// ); - -// test.skip( -// "from optional to required columns should swap", -// async (page) => { -// await restore_viewer(page, { -// settings: true, -// group_by: ["State"], -// columns: [ -// "Profit", -// "Sales", -// null, -// null, -// "Quantity", -// "Discount", -// "Category", -// ], -// }); - -// const origin = await shadow_elem( -// page, -// `#active-columns [data-index="5"] .column-selector-draggable` -// ); - -// const target = await shadow_elem( -// page, -// `#active-columns [data-index="1"]` -// ); - -// await drag(page, origin, target); -// return await get_contents(page); -// }, -// { reload_page: true } -// ); -// test.skip( -// "filter in should work", -// async (page) => { -// await restore_viewer(page, { -// settings: true, -// group_by: [], -// columns: [ -// "Order ID", -// "City", -// null, -// null, -// "Category", -// ], -// }); -// const origin = await shadow_elem( -// page, -// `#active-columns [data-index="1"] .column-selector-draggable` -// ); -// const target = await shadow_elem(page, "#filter"); -// await drag_and_drop(page, origin, target); -// await page.evaluateHandle(async () => { -// const mouseEvent = -// document.createEvent("MouseEvents"); -// mouseEvent.initEvent("focus", true, true); -// const input = document -// .querySelector("perspective-viewer") -// .shadowRoot.querySelector( -// '[placeholder="Value"]' -// ); -// input.dispatchEvent(mouseEvent); -// }); -// await page.evaluateHandle(async () => { -// const op = document -// .querySelector("perspective-viewer") -// .shadowRoot.querySelector( -// ".filterop-selector" -// ); -// op.value = "in"; -// const input = document -// .querySelector("perspective-viewer") -// .shadowRoot.querySelector( -// '[placeholder="Value"]' -// ); -// input.dispatchEvent( -// new Event("change", { -// bubbles: true, -// cancelable: true, -// }) -// ); -// }); -// await page.evaluateHandle(async () => { -// await sleep(500); -// const input = document -// .querySelector("perspective-viewer") -// .shadowRoot.querySelector( -// '[placeholder="Value"]' -// ); -// input.value = "a,Ch"; -// input.dispatchEvent( -// new Event("input", { -// bubbles: true, -// cancelable: true, -// }) -// ); -// function sleep(time) { -// return new Promise((resolve) => -// setTimeout(resolve, time) -// ); -// } -// await sleep(500); -// }); -// return await get_contents(page); -// }, -// { reload_page: true } -// ); -// }); -// }, -// { root: path.join(__dirname, "..", "..") } -// ); -// }); -// }); diff --git a/rust/perspective-viewer/test/js/dragdrop/cancel.spec.ts b/rust/perspective-viewer/test/js/dragdrop/cancel.spec.ts new file mode 100644 index 0000000000..65542793f3 --- /dev/null +++ b/rust/perspective-viewer/test/js/dragdrop/cancel.spec.ts @@ -0,0 +1,200 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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 { compareContentsToSnapshot } from "@perspective-dev/test"; +import { PageView } from "@perspective-dev/test"; +import { + ACTIVE_DRAG, + getSettingsPanelContents, + INACTIVE_DRAG, + shadowDragCancel, +} from "./dragdrop_test_utils"; + +test.beforeEach(async ({ page }) => { + await page.goto( + "/rust/perspective-viewer/test/html/column-settings-enabled.html", + ); + await page.evaluate(async () => { + while (!(window as any)["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); +}); + +test.describe("Drag and Drop", () => { + // Cancel — drag starts but no drop occurs. + // These verify the panel returns to its original state with no side-effects. + test.describe("cancel", () => { + test("drag inactive column and cancel (dragend with no drop)", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + await shadowDragCancel( + page, + view.container.locator(INACTIVE_DRAG).first(), + ); + + const config = await view.save(); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("drag active column and cancel (dragend with no drop)", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + await shadowDragCancel( + page, + view.container.locator(ACTIVE_DRAG).first(), + ); + + const config = await view.save(); + expect(config.columns).toEqual(["Sales", "Profit"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("drag inactive column over wrong target (status bar) then cancel", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + + // Hover over the status bar which is not a valid drop target. + await shadowDragCancel( + page, + view.container.locator(INACTIVE_DRAG).first(), + view.container.locator("#status_bar"), + ); + + const config = await view.save(); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("drag inactive column over Group By then cancel before drop", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + await shadowDragCancel( + page, + view.container.locator(INACTIVE_DRAG).first(), + view.container.locator("#group_by"), + ); + + const config = await view.save(); + expect(config.group_by).toEqual([]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("drag inactive column over Split By then cancel before drop", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + await shadowDragCancel( + page, + view.container.locator(INACTIVE_DRAG).first(), + view.container.locator("#split_by"), + ); + const config = await view.save(); + expect(config.split_by).toEqual([]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("drag inactive column over Sort then cancel before drop", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + await shadowDragCancel( + page, + view.container.locator(INACTIVE_DRAG).first(), + view.container.locator("#sort"), + ); + const config = await view.save(); + expect(config.sort).toEqual([]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("drag inactive column over Filter then cancel before drop", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + await shadowDragCancel( + page, + view.container.locator(INACTIVE_DRAG).first(), + view.container.locator("#filter"), + ); + const config = await view.save(); + expect(config.filter).toEqual([]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("drag inactive column outside component (simulate drop outside browser)", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + + // Start a drag then immediately cancel — no hover over any target. + await shadowDragCancel( + page, + view.container.locator(INACTIVE_DRAG).first(), + ); + + const config = await view.save(); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("drag active column outside component (simulate drop outside browser)", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + await shadowDragCancel( + page, + view.container.locator(ACTIVE_DRAG).first(), + ); + + const config = await view.save(); + expect(config.columns).toEqual(["Sales", "Profit"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + }); +}); diff --git a/rust/perspective-viewer/test/js/dragdrop/dragdrop_test_utils.ts b/rust/perspective-viewer/test/js/dragdrop/dragdrop_test_utils.ts new file mode 100644 index 0000000000..15ae471ef2 --- /dev/null +++ b/rust/perspective-viewer/test/js/dragdrop/dragdrop_test_utils.ts @@ -0,0 +1,88 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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 { Locator, Page } from "@playwright/test"; + +/** The draggable handle of the first visible inactive column. */ +export const INACTIVE_DRAG = + "#sub-columns .column-selector-column:not(.column-selector-column-hidden) .column-selector-draggable"; + +/** The draggable handle of the first active column. */ +export const ACTIVE_DRAG = + "#active-columns .column-selector-column .column-selector-draggable"; + +/** Returns the `#settings_panel` innerHTML from the viewer's shadow root. */ +export async function getSettingsPanelContents(page: Page) { + return page.evaluate(() => { + const viewer = document.querySelector("perspective-viewer")!; + return viewer.shadowRoot!.querySelector("#settings_panel")!.innerHTML; + }); +} + +/** + * Initiates a real browser drag from `src` to `tgt` using mouse events, without + * completing the drop. The drag is left in-flight so the caller can snapshot the + * intermediate dragover UI state. + */ +export async function shadowDragOver(page: Page, src: Locator, tgt: Locator) { + const srcBox = (await src.boundingBox())!; + const tgtBox = (await tgt.boundingBox())!; + const srcX = srcBox.x + srcBox.width / 2; + const srcY = srcBox.y + srcBox.height / 2; + const tgtX = tgtBox.x + tgtBox.width / 2; + const tgtY = tgtBox.y + tgtBox.height / 2; + + await page.mouse.move(srcX, srcY); + await page.mouse.down(); + // Small initial move to trigger the browser's drag-start threshold. + await page.mouse.move(srcX + 5, srcY, { steps: 2 }); + // Move to the target center, generating dragenter + dragover events. + await page.mouse.move(tgtX, tgtY, { steps: 10 }); + // Allow Yew to process events and re-render. + await page.waitForTimeout(100); +} + +/** + * Initiates a real browser drag from `src`, optionally hovers over + * `wrongTgt`, then presses Escape to cancel — firing `dragend` without a + * `drop`. + */ +export async function shadowDragCancel( + page: Page, + src: Locator, + wrongTgt: Locator | null = null, +) { + const srcBox = (await src.boundingBox())!; + const srcX = srcBox.x + srcBox.width / 2; + const srcY = srcBox.y + srcBox.height / 2; + + await page.mouse.move(srcX, srcY); + await page.mouse.down(); + await page.mouse.move(srcX + 5, srcY, { steps: 2 }); + + if (wrongTgt) { + const wrongBox = (await wrongTgt.boundingBox())!; + await page.mouse.move( + wrongBox.x + wrongBox.width / 2, + wrongBox.y + wrongBox.height / 2, + { steps: 10 }, + ); + } + + // Cancel the drag (fires dragend with no preceding drop). + await page.keyboard.press("Escape"); + await page.waitForTimeout(100); +} + +export async function localDrag(page: any, source: any, target: any) { + await source.dragTo(target, { steps: 100 }); +} diff --git a/rust/perspective-viewer/test/js/dragdrop/dragover.spec.ts b/rust/perspective-viewer/test/js/dragdrop/dragover.spec.ts new file mode 100644 index 0000000000..d9ccc96df1 --- /dev/null +++ b/rust/perspective-viewer/test/js/dragdrop/dragover.spec.ts @@ -0,0 +1,189 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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 { compareContentsToSnapshot } from "../helpers.ts"; +import { PageView } from "@perspective-dev/test"; +import { + ACTIVE_DRAG, + getSettingsPanelContents, + INACTIVE_DRAG, + localDrag, + shadowDragOver, +} from "./dragdrop_test_utils"; + +test.beforeEach(async ({ page }) => { + await page.goto( + "/rust/perspective-viewer/test/html/column-settings-enabled.html", + ); + await page.evaluate(async () => { + while (!(window as any)["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); +}); + +test.describe("Drag and Drop", () => { + // Drag over a target without releasing (no drop) + // Tests the intermediate UI state — dragover highlights, etc. + test.describe("dragover (without drop)", () => { + test("inactive column dragged over Active Columns", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + await shadowDragOver( + page, + view.container.locator(INACTIVE_DRAG).first(), + view.container + .locator("#active-columns .column-selector-column") + .nth(0), + ); + + const config = await view.save(); + expect(config.columns).toEqual(["Sales"]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await page.pause(); + await compareContentsToSnapshot(contents); + }); + + test("inactive column dragged over Group By", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + await shadowDragOver( + page, + view.container.locator(INACTIVE_DRAG).first(), + view.container.locator("#group_by"), + ); + const config = await view.save(); + expect(config.group_by).toEqual([]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive column dragged over Split By", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + await shadowDragOver( + page, + view.container.locator(INACTIVE_DRAG).first(), + view.container.locator("#split_by"), + ); + const config = await view.save(); + expect(config.split_by).toEqual([]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive column dragged over Sort", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + await shadowDragOver( + page, + view.container.locator(INACTIVE_DRAG).first(), + view.container.locator("#sort"), + ); + const config = await view.save(); + expect(config.sort).toEqual([]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive column dragged over Filter", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + await shadowDragOver( + page, + view.container.locator(INACTIVE_DRAG).first(), + view.container.locator("#filter"), + ); + const config = await view.save(); + expect(config.filter).toEqual([]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("active column dragged over Group By", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + await shadowDragOver( + page, + view.container.locator(ACTIVE_DRAG).first(), + view.container.locator("#group_by"), + ); + const config = await view.save(); + expect(config.group_by).toEqual([]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("active column dragged over Split By", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + await shadowDragOver( + page, + view.container.locator(ACTIVE_DRAG).first(), + view.container.locator("#split_by"), + ); + const config = await view.save(); + expect(config.split_by).toEqual([]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("active column dragged over Sort", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + await shadowDragOver( + page, + view.container.locator(ACTIVE_DRAG).first(), + view.container.locator("#sort"), + ); + const config = await view.save(); + expect(config.sort).toEqual([]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("active column dragged over Filter", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + await shadowDragOver( + page, + view.container.locator(ACTIVE_DRAG).first(), + view.container.locator("#filter"), + ); + + const config = await view.save(); + expect(config.filter).toEqual([]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + }); +}); diff --git a/rust/perspective-viewer/test/js/dragdrop/drop.spec.ts b/rust/perspective-viewer/test/js/dragdrop/drop.spec.ts new file mode 100644 index 0000000000..e3c0b2376a --- /dev/null +++ b/rust/perspective-viewer/test/js/dragdrop/drop.spec.ts @@ -0,0 +1,182 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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 { compareContentsToSnapshot } from "../helpers.ts"; +import { PageView } from "@perspective-dev/test"; +import { + ACTIVE_DRAG, + getSettingsPanelContents, + INACTIVE_DRAG, + localDrag, +} from "./dragdrop_test_utils"; + +test.beforeEach(async ({ page }) => { + await page.goto( + "/rust/perspective-viewer/test/html/column-settings-enabled.html", + ); + await page.evaluate(async () => { + while (!(window as any)["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); +}); + +test.describe("Drag and Drop", () => { + test.describe("drop", () => { + test("inactive to first active columns", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container + .locator("#active-columns .column-selector-column") + .nth(0); + + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.columns).toEqual(["Category", "Sales", "Profit"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to second active columns", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container + .locator("#active-columns .column-selector-column") + .nth(1); + + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.columns).toEqual(["Sales", "Category", "Profit"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("active to active (reorder)", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit", "Quantity"], + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(ACTIVE_DRAG).first(); + + // Drop the first active column onto the third position. + const target = view.container + .locator("#active-columns .column-selector-column") + .nth(2); + + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.columns).toEqual(["Profit", "Quantity", "Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Group By", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#group_by"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.group_by).toEqual(["Category"]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Split By", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#split_by"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.split_by).toEqual(["Category"]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Sort", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#sort"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.columns).toEqual(["Sales"]); + expect(config.sort).toEqual([["Category", "asc"]]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Filter", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#filter"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.filter).toEqual([["Category", "==", null]]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + }); +}); diff --git a/rust/perspective-viewer/test/js/dragdrop/drop_column_settings.spec.ts b/rust/perspective-viewer/test/js/dragdrop/drop_column_settings.spec.ts new file mode 100644 index 0000000000..483b13bda8 --- /dev/null +++ b/rust/perspective-viewer/test/js/dragdrop/drop_column_settings.spec.ts @@ -0,0 +1,209 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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 { compareContentsToSnapshot } from "@perspective-dev/test"; +import { PageView } from "@perspective-dev/test"; +import { + ACTIVE_DRAG, + getSettingsPanelContents, + INACTIVE_DRAG, + localDrag, +} from "./dragdrop_test_utils"; + +test.beforeEach(async ({ page }) => { + await page.goto( + "/rust/perspective-viewer/test/html/column-settings-enabled.html", + ); + await page.evaluate(async () => { + while (!(window as any)["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); +}); + +test.describe("Drag and Drop", () => { + test.describe("drop with Column Settings Sidebar open", () => { + test("inactive to active while sidebar is open on the active column", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + + // Open the sidebar on the current active column. + const activeCol = + view.settingsPanel.activeColumns.getFirstVisibleColumn(); + + await activeCol.editBtn.click(); + await view.columnSettingsSidebar.container.waitFor({ + state: "visible", + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = activeCol.container; + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.columns).toEqual(["Category", "Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("active to Group By while sidebar is open on the dragged column", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Category", "Profit"], + }); + + // Open the sidebar on the first active column (Sales). + const activeCol = + view.settingsPanel.activeColumns.getFirstVisibleColumn(); + await activeCol.editBtn.click(); + await view.columnSettingsSidebar.container.waitFor({ + state: "visible", + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(ACTIVE_DRAG).first(); + const target = view.container.locator("#group_by"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.group_by).toEqual(["Category"]); + expect(config.columns).toEqual(["Profit"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + // TODO + + test("inactive to Group By while sidebar is open on a different column", async ({ + page, + }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + // Open sidebar on first active column. + const activeCol = + view.settingsPanel.activeColumns.getFirstVisibleColumn(); + await activeCol.editBtn.click(); + await view.columnSettingsSidebar.container.waitFor({ + state: "visible", + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#group_by"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.group_by).toEqual(["Category"]); + expect(config.columns).toEqual(["Sales", "Profit"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Split By while sidebar is open", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + + const activeCol = + view.settingsPanel.activeColumns.getFirstVisibleColumn(); + await activeCol.editBtn.click(); + await view.columnSettingsSidebar.container.waitFor({ + state: "visible", + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#split_by"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.split_by).toEqual(["Category"]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Filter while sidebar is open", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + + const activeCol = + view.settingsPanel.activeColumns.getFirstVisibleColumn(); + await activeCol.editBtn.click(); + await view.columnSettingsSidebar.container.waitFor({ + state: "visible", + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#filter"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.filter).toEqual([["Category", "==", null]]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Sort while sidebar is open", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + + const activeCol = + view.settingsPanel.activeColumns.getFirstVisibleColumn(); + await activeCol.editBtn.click(); + await view.columnSettingsSidebar.container.waitFor({ + state: "visible", + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#sort"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.sort).toEqual([["Category", "asc"]]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + }); +}); diff --git a/rust/perspective-viewer/test/js/dragdrop/drop_named_columns.spec.ts b/rust/perspective-viewer/test/js/dragdrop/drop_named_columns.spec.ts new file mode 100644 index 0000000000..8abbf4e2e1 --- /dev/null +++ b/rust/perspective-viewer/test/js/dragdrop/drop_named_columns.spec.ts @@ -0,0 +1,229 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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 { compareContentsToSnapshot } from "../helpers.ts"; +import { PageView } from "@perspective-dev/test"; +import { + ACTIVE_DRAG, + getSettingsPanelContents, + INACTIVE_DRAG, + localDrag, +} from "./dragdrop_test_utils"; + +test.beforeEach(async ({ page }) => { + await page.goto( + "/rust/perspective-viewer/test/html/column-selector-modes.html", + ); + await page.evaluate(async () => { + while (!(window as any)["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); +}); + +test.describe("Drag and Drop", () => { + test.describe("drop", () => { + test("inactive to first active columns", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container + .locator("#active-columns .column-selector-column") + .nth(0); + + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.columns).toEqual([ + "Category", + "Profit", + null, + null, + null, + ]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to second active columns", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container + .locator("#active-columns .column-selector-column") + .nth(1); + + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.columns).toEqual([ + "Sales", + "Category", + null, + null, + null, + ]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to third active columns", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit"], + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container + .locator("#active-columns .column-selector-column") + .nth(2); + + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.columns).toEqual([ + "Sales", + "Profit", + "Category", + null, + null, + ]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("active to active (reorder)", async ({ page }) => { + const view = new PageView(page); + await view.restore({ + settings: true, + columns: ["Sales", "Profit", "Quantity"], + }); + + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(ACTIVE_DRAG).first(); + const target = view.container + .locator("#active-columns .column-selector-column") + .nth(2); + + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.columns).toEqual([ + "Quantity", + "Profit", + "Sales", + null, + null, + ]); + + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Group By", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#group_by"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.group_by).toEqual(["Category"]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Split By", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#split_by"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.split_by).toEqual(["Category"]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Sort", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#sort"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.columns).toEqual(["Sales"]); + expect(config.sort).toEqual([["Category", "asc"]]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + + test("inactive to Filter", async ({ page }) => { + const view = new PageView(page); + await view.restore({ settings: true, columns: ["Sales"] }); + const configUpdated = await view.getEventListener( + "perspective-config-update", + ); + + const source = view.container.locator(INACTIVE_DRAG).first(); + const target = view.container.locator("#filter"); + await localDrag(page, source, target); + await configUpdated(); + const config = await view.save(); + expect(config.filter).toEqual([["Category", "==", null]]); + expect(config.columns).toEqual(["Sales"]); + const contents = await getSettingsPanelContents(page); + await compareContentsToSnapshot(contents); + }); + }); +}); diff --git a/rust/perspective-viewer/test/js/errors.spec.js b/rust/perspective-viewer/test/js/errors.spec.js deleted file mode 100644 index 07d91c1698..0000000000 --- a/rust/perspective-viewer/test/js/errors.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ 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("/rust/perspective-viewer/test/html/blank.html"); - await page.waitForFunction(() => "WORKER" in window); -}); - -test.describe("viewer.load() method", async () => { - test("does load a resolved Table promise", async ({ page }) => { - const viewer = page.locator("perspective-viewer"); - await viewer.evaluate(async (viewer) => { - const goodTable = (await window.WORKER).table("a,b,c\n1,2,3"); - return viewer.load(goodTable); - }); - await expect(viewer).toHaveText(/"a","b","c"/); // column titles - }); - - test("is rejected by a rejected Table promise", async ({ page }) => { - const viewer = page.locator("perspective-viewer"); - await expect( - viewer.evaluate((viewer) => { - const errorTable = Promise.reject(new Error("blimpy")); - return viewer.load(errorTable); - }), - ).rejects.toThrow("blimpy"); - }); - - test("after a load error, same viewer can load a resolved Table promise", async ({ - page, - }) => { - const viewer = page.locator("perspective-viewer"); - const didError = await viewer.evaluate(async (viewer) => { - const errorTable = Promise.reject(new Error("blimpy")); - const worker = await window.WORKER; - let didError = false; - try { - await viewer.load(errorTable); - } catch (e) { - if (e.message.includes("blimpy")) { - didError = true; - } - } - - const goodTable = worker.table("a,b,c\n1,2,3"); - await viewer.load(goodTable); - return didError; - }); - expect(didError).toBe(true); - }); -}); diff --git a/rust/perspective-viewer/test/js/helpers.ts b/rust/perspective-viewer/test/js/helpers.ts new file mode 100644 index 0000000000..3414c59f30 --- /dev/null +++ b/rust/perspective-viewer/test/js/helpers.ts @@ -0,0 +1,129 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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 * as fs from "node:fs"; +import * as path from "node:path"; +import * as url from "node:url"; +import type { ViewerConfigUpdate } from "@perspective-dev/viewer"; +import type { Locator, Page } from "@playwright/test"; +import * as prettier from "prettier"; + +// Re-export test framework (preserves consoleLogs auto-fixture) +export { test, expect } from "@perspective-dev/test"; + +// Re-export page object models +export { PageView } from "@perspective-dev/test"; +export { ColumnSettingsSidebar } from "@perspective-dev/test/src/js/models/column_settings.ts"; +export { ColumnSelector } from "@perspective-dev/test/src/js/models/settings_panel.ts"; + +const __filename = url.fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const API_VERSION: string = JSON.parse( + fs + .readFileSync( + path.resolve(__dirname, "../../../../tools/test/package.json"), + ) + .toString(), +)["version"]; + +export const DEFAULT_CONFIG: ViewerConfigUpdate = { + aggregates: {}, + columns_config: {}, + columns: [], + expressions: {}, + filter: [], + group_by: [], + group_rollup_mode: "rollup", + plugin: "", + plugin_config: {}, + settings: false, + sort: [], + split_by: [], + version: API_VERSION, + table: "load-viewer-csv", + title: null, + theme: "Pro Light", +}; + +export async function compareContentsToSnapshot( + contents: string, + extraSnapshotPath?: string[], +): Promise { + const { expect, test } = await import("@playwright/test"); + let titlePath = test.info().titlePath; + if (extraSnapshotPath) { + titlePath = titlePath.concat(extraSnapshotPath); + } + + const snapshotPath = [ + titlePath + .slice(1) + .map((s) => + s + .trim() + .replace(/[^a-z0-9]+/gi, "-") + .toLowerCase(), + ) + .join("-") + ".txt", + ]; + + const pathArray = Array.isArray(snapshotPath) + ? snapshotPath + : [snapshotPath]; + + const cleanedContents = contents + .replace(/style=""/g, "") + .replace(/(min-|max-)?(width|height): *\d+\.*\d+(px)?;? */g, ""); + + const formatted = await prettier.format(cleanedContents, { + parser: "html", + }); + + await expect(formatted).toMatchSnapshot(pathArray); +} + +export async function compareNodes( + left: Locator, + right: Locator, + page: Page, +): Promise { + const leftEl = await left.elementHandle(); + const rightEl = await right.elementHandle(); + return await page.evaluate( + async (compare) => { + return compare.leftEl?.isEqualNode(compare.rightEl) || false; + }, + { leftEl, rightEl }, + ); +} + +export async function getShadowContents(page: Page): Promise { + const raw = await page.evaluate(async () => { + const viewer = + document.querySelector("perspective-viewer")!.shadowRoot!; + return viewer.innerHTML; + }); + + return await prettier.format(raw, { + parser: "html", + }); +} + +export async function getLightContents(page: Page): Promise { + return await page.evaluate(async () => { + const viewer = document.querySelector( + "perspective-viewer perspective-viewer-plugin", + )!; + return viewer.innerHTML; + }); +} diff --git a/rust/perspective-viewer/test/js/helpers/standard_tests.ts b/rust/perspective-viewer/test/js/helpers/standard_tests.ts new file mode 100644 index 0000000000..31a3a72a10 --- /dev/null +++ b/rust/perspective-viewer/test/js/helpers/standard_tests.ts @@ -0,0 +1,200 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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 type { ViewerConfigUpdate } from "@perspective-dev/viewer"; +import { test, compareContentsToSnapshot } from "../helpers.ts"; + +export type ContentExtractor = (page: any) => Promise; + +async function restoreViewer(page: any, viewerConfig: ViewerConfigUpdate) { + return await page.evaluate(async (viewerConfig: ViewerConfigUpdate) => { + const viewer = document.querySelector("perspective-viewer")!; + await viewer.restore(viewerConfig); + }, viewerConfig); +} + +function runSimpleCompareTest( + viewerConfig: ViewerConfigUpdate, + extractContent: ContentExtractor, + snapshotPath: string[], +) { + return async ({ page }: { page: any }) => { + await restoreViewer(page, viewerConfig); + const content = await extractContent(page); + await compareContentsToSnapshot(content); + }; +} + +export function run_standard_tests( + context: string, + extractContent: ContentExtractor, +) { + test("grid > renders without settings panel", async ({ page }) => { + await page.evaluate(async () => { + const viewer = document.querySelector("perspective-viewer")!; + await viewer.getTable(); + await viewer.restore({ settings: true }); + }); + + const content = await extractContent(page); + await compareContentsToSnapshot(content); + }); + + test("columns > displays only visible columns", async ({ page }) => { + await restoreViewer(page, { + columns: ["Discount", "Profit", "Sales", "Quantity"], + }); + + const visibleColumnContent = await extractContent(page); + await compareContentsToSnapshot(visibleColumnContent); + }); + + test.describe("Pivot tests", () => { + test( + "group_by > pivots by a single row", + runSimpleCompareTest( + { group_by: ["State"], settings: true }, + extractContent, + [context, `pivot-by-row.txt`], + ), + ); + + test( + "group_by > pivots by two rows", + runSimpleCompareTest( + { + group_by: ["Category", "Sub-Category"], + settings: true, + }, + extractContent, + [context, `pivot-by-two-rows.txt`], + ), + ); + + test( + "split_by > pivots by a single column", + runSimpleCompareTest( + { split_by: ["Category"], settings: true }, + extractContent, + [context, `pivot-by-column.txt`], + ), + ); + + test( + "pivot > pivots by a row and a column", + runSimpleCompareTest( + { + group_by: ["State"], + split_by: ["Category"], + settings: true, + }, + extractContent, + [context, `pivot-by-row-and-column.txt`], + ), + ); + + test( + "pivot > pivots by two rows and two columns", + runSimpleCompareTest( + { + group_by: ["Region", "State"], + split_by: ["Category", "Sub-Category"], + settings: true, + }, + extractContent, + [context, `pivot-by-two-rows-and-two-columns.txt`], + ), + ); + }); + + test.describe("Sort tests", () => { + test( + "sort > sorts by a hidden column", + runSimpleCompareTest( + { + columns: ["Row ID", "Quantity"], + sort: [["Sales", "asc"]], + settings: true, + }, + extractContent, + [context, `sort-by-hidden-column.txt`], + ), + ); + + test( + "sort > sorts by a numeric column", + runSimpleCompareTest( + { + columns: ["Row ID", "Sales"], + sort: [["Quantity", "asc"]], + settings: true, + }, + extractContent, + [context, `sort-by-numeric-column.txt`], + ), + ); + + test( + "sort > sorts by an alpha column", + runSimpleCompareTest( + { + columns: ["Row ID", "State", "Sales"], + sort: [["State", "asc"]], + settings: true, + }, + extractContent, + [context, `sort-by-alpha-column.txt`], + ), + ); + }); + + test.describe("Filter tests", () => { + test( + "filter > filters by a numeric column", + runSimpleCompareTest( + { + columns: ["Row ID", "State", "Sales"], + filter: [["Sales", ">", 500]], + settings: true, + }, + extractContent, + [context, `filter-by-numeric-column.txt`], + ), + ); + + test( + "filter > filters by an alpha column", + runSimpleCompareTest( + { + columns: ["Row ID", "State", "Sales"], + filter: [["State", "==", "Texas"]], + settings: true, + }, + extractContent, + [context, `filter-by-alpha-column.txt`], + ), + ); + + test( + "filter > filters with 'in' comparator", + runSimpleCompareTest( + { + columns: ["Row ID", "State", "Sales"], + filter: [["State", "in", ["Texas", "California"]]], + settings: true, + }, + extractContent, + [context, `filter-with-in-comparator.txt`], + ), + ); + }); +} diff --git a/rust/perspective-viewer/test/js/render_warning_tests.js b/rust/perspective-viewer/test/js/render_warning_tests.js deleted file mode 100644 index ff1cbf07d2..0000000000 --- a/rust/perspective-viewer/test/js/render_warning_tests.js +++ /dev/null @@ -1,246 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ 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). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -const convert = (x) => - ({ - "X/Y Scatter": "perspective-viewer-d3fc-xyscatter", - "Y Scatter": "Perspective-viewer-d3fc-yscatter", - "Y Line": "perspective-viewer-d3fc-yline", - Heatmap: "perspective-viewer-d3fc-heatmap", - Treemap: "perspective-viewer-d3fc-treemap", - "X Bar": "perspective-viewer-d3fc-xbar", - "Y Bar": "perspective-viewer-d3fc-ybar", - })[x] || x; - -exports.default = function (plugin_name, columns) { - let view_columns = ["Sales"]; - - if (columns) { - // Handle cases where multiple columns are required for a proper - // chart render, i.e. in scatter charts where there is an X and Y axis. - view_columns = columns; - } - - view_columns = JSON.stringify(view_columns); - - test.capture( - "warning should be shown when points exceed max_cells and max_columns", - async (page) => { - const viewer = await page.$("perspective-viewer"); - await page.evaluate((plugin_name) => { - const plugin = document.querySelector(plugin_name); - plugin.max_columns = 1; - plugin.max_cells = 1; - }, convert(plugin_name)); - - await page.evaluate( - async () => - await document - .querySelector("perspective-viewer") - .toggleConfig(), - ); - await page.evaluate( - (element, view_columns) => - element.setAttribute("columns", view_columns), - viewer, - view_columns, - ); - await page.evaluate( - (element) => - element.setAttribute("column-pivots", '["Profit"]'), - viewer, - ); - await page.waitForSelector("perspective-viewer:not([updating])"); - }, - ); - - test.capture( - "warning should be shown when points exceed max_cells but not max_columns", - async (page) => { - const viewer = await page.$("perspective-viewer"); - await page.evaluate((plugin_name) => { - const plugin = document.querySelector(plugin_name); - plugin.max_cells = 1; - }, convert(plugin_name)); - - await page.evaluate( - async () => - await document - .querySelector("perspective-viewer") - .toggleConfig(), - ); - await page.evaluate( - (element, view_columns) => - element.setAttribute("columns", view_columns), - viewer, - view_columns, - ); - await page.evaluate( - (element) => - element.setAttribute("column-pivots", '["Profit"]'), - viewer, - ); - await page.waitForSelector("perspective-viewer:not([updating])"); - }, - ); - - test.capture( - "warning should be shown when points exceed max_columns but not max_cells", - async (page) => { - const viewer = await page.$("perspective-viewer"); - await page.evaluate((plugin_name) => { - const plugin = document.querySelector(plugin_name); - plugin.max_columns = 1; - }, convert(plugin_name)); - - await page.evaluate( - async () => - await document - .querySelector("perspective-viewer") - .toggleConfig(), - ); - await page.evaluate( - (element, view_columns) => - element.setAttribute("columns", view_columns), - viewer, - view_columns, - ); - await page.evaluate( - (element) => - element.setAttribute("column-pivots", '["Profit"]'), - viewer, - ); - await page.waitForSelector("perspective-viewer:not([updating])"); - }, - ); - - test.capture( - "warning should be re-rendered if the config is changed and points exceed max", - async (page) => { - const viewer = await page.$("perspective-viewer"); - await page.evaluate((plugin_name) => { - const plugin = document.querySelector(plugin_name); - plugin.max_columns = 1; - }, convert(plugin_name)); - - await page.evaluate( - async () => - await document - .querySelector("perspective-viewer") - .toggleConfig(), - ); - await page.evaluate( - (element, view_columns) => - element.setAttribute("columns", view_columns), - viewer, - view_columns, - ); - await page.evaluate( - (element) => - element.setAttribute("column-pivots", '["Row ID"]'), - viewer, - ); - await page.waitForSelector("perspective-viewer:not([updating])"); - await page.evaluate( - (element) => - element.setAttribute("column-pivots", '["Profit"]'), - viewer, - ); - await page.waitForSelector("perspective-viewer:not([updating])"); - }, - ); - - test.capture( - "warning should not be re-rendered if the config is changed and points do not exceed max", - async (page) => { - const viewer = await page.$("perspective-viewer"); - await page.evaluate((plugin_name) => { - const plugin = document.querySelector(plugin_name); - plugin.max_columns = 5; - }, convert(plugin_name)); - - await page.evaluate( - async () => - await document - .querySelector("perspective-viewer") - .toggleConfig(), - ); - await page.evaluate( - (element, view_columns) => - element.setAttribute("columns", view_columns), - viewer, - view_columns, - ); - await page.evaluate( - (element) => - element.setAttribute("column-pivots", '["Profit"]'), - viewer, - ); - await page.waitForSelector("perspective-viewer:not([updating])"); - await page.evaluate( - (element) => element.removeAttribute("column-pivots"), - viewer, - ); - await page.waitForSelector("perspective-viewer:not([updating])"); - }, - ); - - test.capture( - "warning should not be rendered again after the user clicks render all points", - async (page) => { - const viewer = await page.$("perspective-viewer"); - await page.evaluate((plugin_name) => { - const plugin = document.querySelector(plugin_name); - plugin.max_columns = 1; - }, convert(plugin_name)); - - await page.evaluate( - async () => - await document - .querySelector("perspective-viewer") - .toggleConfig(), - ); - await page.evaluate( - (element, view_columns) => - element.setAttribute("columns", view_columns), - viewer, - view_columns, - ); - await page.evaluate( - (element) => - element.setAttribute("column-pivots", '["Row ID"]'), - viewer, - ); - await page.waitForSelector("perspective-viewer:not([updating])"); - await page.waitFor( - () => - !!document - .querySelector("perspective-viewer") - .shadowRoot.querySelector("perspective-vieux") - .shadowRoot.querySelector( - ".plugin_information--warning:not(.hidden)", - ), - ); - await page.shadow_click( - "perspective-viewer", - "perspective-vieux", - ".plugin_information__action", - ); - await page.evaluate( - (element) => - element.setAttribute("column-pivots", '["Profit"]'), - viewer, - ); - await page.waitForSelector("perspective-viewer:not([updating])"); - }, - ); -}; diff --git a/rust/perspective-viewer/test/js/settings.spec.js b/rust/perspective-viewer/test/js/settings.spec.js deleted file mode 100644 index 9208e5d7aa..0000000000 --- a/rust/perspective-viewer/test/js/settings.spec.js +++ /dev/null @@ -1,119 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ 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 { compareContentsToSnapshot } from "@perspective-dev/test"; -import * as prettier from "prettier"; - -async function get_contents(page) { - const raw = await page.evaluate(async () => { - const viewer = document.querySelector("perspective-viewer").shadowRoot; - return viewer.innerHTML; - }); - - return await prettier.format(raw, { - parser: "html", - }); -} - -test.describe("Settings", () => { - test.describe("Toggle", () => { - test.beforeEach(async ({ page }) => { - await page.goto( - "/rust/perspective-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("opens settings when field is set to true", async ({ page }) => { - await page.evaluate(async () => { - const viewer = document.querySelector("perspective-viewer"); - await viewer.getTable(); - await viewer.restore({ settings: true }); - }); - - const contents = await get_contents(page); - - await compareContentsToSnapshot(contents, [ - "opens-settings-when-field-is-set-to-true.txt", - ]); - }); - - test("opens settings when field is set to false", async ({ page }) => { - await page.evaluate(async () => { - const viewer = document.querySelector("perspective-viewer"); - await viewer.getTable(); - await viewer.restore({ settings: false }); - }); - - const contents = await get_contents(page); - - await compareContentsToSnapshot(contents, [ - "opens-settings-when-field-is-set-to-false.txt", - ]); - }); - }); - - test.describe("Regressions", () => { - test("load and restore with settings called at the same time does not throw", async ({ - page, - consoleLogs, - }) => { - const errors = []; - page.on("pageerror", async (msg) => { - errors.push(`${msg.name}::${msg.message}`); - }); - - await page.goto("/rust/perspective-viewer/test/html/blank.html", { - waitUntil: "networkidle", - }); - - await page.evaluate(async () => { - const viewer = document.querySelector("perspective-viewer"); - viewer.load( - new Promise((_, reject) => - reject("Intentional Load Error"), - ), - ); - try { - await viewer.restore({ - settings: true, - plugin: "Debug", - }); - } catch (e) { - // We need to catch this error else the `evaluate()` fails. - // We need to await the call because we want it to fail - // before continuing the test. - console.error("Caught error:", e); - } - - await new Promise((x) => setTimeout(x, 1000)); - }); - - const contents = await get_contents(page); - expect(errors).toEqual([ - 'Error::Failed to construct table from JsValue("Intentional Load Error")', - ]); - consoleLogs.expectedLogs.push("error", /Intentional Load Error/); - }); - }); -}); diff --git a/rust/perspective-viewer/test/js/settings_panel/close.spec.ts b/rust/perspective-viewer/test/js/settings_panel/close.spec.ts new file mode 100644 index 0000000000..6cb0b63acc --- /dev/null +++ b/rust/perspective-viewer/test/js/settings_panel/close.spec.ts @@ -0,0 +1,87 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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, DEFAULT_CONFIG, API_VERSION } from "../helpers.ts"; + +test.beforeEach(async ({ page }) => { + await page.goto( + "/node_modules/@perspective-dev/viewer/test/html/plugin-priority-order.html", + ); + await page.evaluate(async () => { + while (!window["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); +}); + +test.describe("Close button", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/rust/perspective-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 () => { + const viewer = document.querySelector("perspective-viewer"); + await viewer.restore({ plugin: "Debug" }); + await viewer.getTable(); + await viewer.restore({ settings: true }); + }); + }); + + test("close button hides the settings panel", async ({ page }) => { + const shadowRoot = page.locator("perspective-viewer"); + await shadowRoot.locator("#settings_close_button").first().click(); + + const saved = await page.evaluate(async () => + document.querySelector("perspective-viewer").save(), + ); + + expect(saved.settings).toBe(false); + }); + + test("settings panel round-trips open/close via save()", async ({ + page, + }) => { + // Open → close via API → open again. save() must reflect each state. + const saved1 = await page.evaluate(async () => + document.querySelector("perspective-viewer").save(), + ); + expect(saved1.settings).toBe(true); + + await page.evaluate(async () => + document.querySelector("perspective-viewer").restore({ + settings: false, + }), + ); + + const saved2 = await page.evaluate(async () => + document.querySelector("perspective-viewer").save(), + ); + + expect(saved2.settings).toBe(false); + + await page.evaluate(async () => + document.querySelector("perspective-viewer").restore({ + settings: true, + }), + ); + + const saved3 = await page.evaluate(async () => + document.querySelector("perspective-viewer").save(), + ); + + expect(saved3.settings).toBe(true); + }); +}); diff --git a/rust/perspective-viewer/test/js/plugins.spec.js b/rust/perspective-viewer/test/js/settings_panel/plugins.spec.ts similarity index 93% rename from rust/perspective-viewer/test/js/plugins.spec.js rename to rust/perspective-viewer/test/js/settings_panel/plugins.spec.ts index 68a8493ce1..7ba1cbdffb 100644 --- a/rust/perspective-viewer/test/js/plugins.spec.js +++ b/rust/perspective-viewer/test/js/settings_panel/plugins.spec.ts @@ -10,8 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { test, expect, DEFAULT_CONFIG } from "@perspective-dev/test"; -import { API_VERSION } from "@perspective-dev/test"; +import { test, expect, DEFAULT_CONFIG, API_VERSION } from "../helpers.ts"; test.beforeEach(async ({ page }) => { await page.goto( @@ -24,13 +23,14 @@ test.beforeEach(async ({ page }) => { }); }); -test.describe("Plugin Priority Order", () => { - test("Elements are loaded in priority Order", async ({ page }) => { +test.describe("Plugin Registration", () => { + test("priority > loads highest priority plugin by default", async ({ + page, + }) => { let saved = await page.evaluate(async () => { const viewer = document.querySelector("perspective-viewer"); window.__TABLE__ = await viewer.getTable(); await viewer.reset(); - return await viewer.save(); }); diff --git a/rust/perspective-viewer/test/js/settings_panel/toggle.spec.ts b/rust/perspective-viewer/test/js/settings_panel/toggle.spec.ts new file mode 100644 index 0000000000..29c5061515 --- /dev/null +++ b/rust/perspective-viewer/test/js/settings_panel/toggle.spec.ts @@ -0,0 +1,59 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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, + compareContentsToSnapshot, + getShadowContents, +} from "../helpers.ts"; + +const get_contents = getShadowContents; + +test.describe("Settings Panel", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/rust/perspective-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("toggle > opens when settings is true", async ({ page }) => { + await page.evaluate(async () => { + const viewer = document.querySelector("perspective-viewer"); + await viewer.getTable(); + await viewer.restore({ settings: true }); + }); + + const contents = await get_contents(page); + await compareContentsToSnapshot(contents); + }); + + test("toggle > stays closed when settings is false", async ({ page }) => { + await page.evaluate(async () => { + const viewer = document.querySelector("perspective-viewer"); + await viewer.getTable(); + await viewer.restore({ settings: false }); + }); + + const contents = await get_contents(page); + await compareContentsToSnapshot(contents); + }); +}); diff --git a/rust/perspective-viewer/test/js/export_table.spec.ts b/rust/perspective-viewer/test/js/stability/export.spec.ts similarity index 93% rename from rust/perspective-viewer/test/js/export_table.spec.ts rename to rust/perspective-viewer/test/js/stability/export.spec.ts index ba7edff0e4..f0b3f1f5af 100644 --- a/rust/perspective-viewer/test/js/export_table.spec.ts +++ b/rust/perspective-viewer/test/js/stability/export.spec.ts @@ -10,7 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { test, expect } from "@perspective-dev/test"; +import { test, expect } from "../helpers.ts"; import * as path from "node:path"; test.beforeEach(async ({ page }) => { @@ -32,8 +32,10 @@ test.beforeEach(async ({ page }) => { }); }); -test.describe("Export button", () => { - test("Single threaded engine mode works", async ({ page }) => { +test.describe("Single-Threaded Engine", () => { + test("render > produces correct output in single-threaded mode", async ({ + page, + }) => { await page.evaluate(async () => { const viewer = document.querySelector("perspective-viewer")!; await viewer.restore({ diff --git a/rust/perspective-viewer/test/js/leaks.spec.js b/rust/perspective-viewer/test/js/stability/leaks.spec.ts similarity index 90% rename from rust/perspective-viewer/test/js/leaks.spec.js rename to rust/perspective-viewer/test/js/stability/leaks.spec.ts index 3a837c31c8..ddb632fb93 100644 --- a/rust/perspective-viewer/test/js/leaks.spec.js +++ b/rust/perspective-viewer/test/js/stability/leaks.spec.ts @@ -10,8 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { test, expect } from "@perspective-dev/test"; -import { compareContentsToSnapshot } from "@perspective-dev/test"; +import { test, expect, compareContentsToSnapshot } from "../helpers.ts"; test.beforeEach(async ({ page }) => { await page.goto("/rust/perspective-viewer/test/html/superstore.html"); @@ -27,9 +26,11 @@ test.beforeEach(async ({ page }) => { }); }); -test.describe("leaks", () => { +test.describe("Memory Leaks", () => { // This originally has a timeout of 120000 - test("doesn't leak elements", async ({ page }) => { + test("elements > no leak on repeated element recreation", async ({ + page, + }) => { let viewer = await page.$("perspective-viewer"); await page.evaluate(async (viewer) => { window.__TABLE__ = await viewer.getTable(); @@ -73,10 +74,10 @@ test.describe("leaks", () => { return element.innerHTML; }); - await compareContentsToSnapshot(contents, ["does-not-leak.txt"]); + await compareContentsToSnapshot(contents); }); - test("doesn't leak views when setting group by", async ({ page }) => { + test("views > no leak on repeated group_by changes", async ({ page }) => { let viewer = await page.$("perspective-viewer"); await page.evaluate(async (viewer) => { window.__TABLE__ = await viewer.getTable(); @@ -119,12 +120,10 @@ test.describe("leaks", () => { return viewer.innerHTML; }, viewer); - await compareContentsToSnapshot(contents, [ - "does-not-leak-when-setting-groupby.txt", - ]); + await compareContentsToSnapshot(contents); }); - test("doesn't leak views when setting filters", async ({ page }) => { + test("views > no leak on repeated filter changes", async ({ page }) => { let viewer = await page.$("perspective-viewer"); await page.evaluate(async (viewer) => { window.__TABLE__ = await viewer.getTable(); @@ -159,8 +158,6 @@ test.describe("leaks", () => { return viewer.innerHTML; }, viewer); - await compareContentsToSnapshot(contents, [ - "does-not-leak-when-setting-filters.txt", - ]); + await compareContentsToSnapshot(contents); }); }); diff --git a/rust/perspective-viewer/test/js/test_arrows.js b/rust/perspective-viewer/test/js/superstore/inline.spec.ts similarity index 64% rename from rust/perspective-viewer/test/js/test_arrows.js rename to rust/perspective-viewer/test/js/superstore/inline.spec.ts index 2f2c9dd8f1..02e209f842 100644 --- a/rust/perspective-viewer/test/js/test_arrows.js +++ b/rust/perspective-viewer/test/js/superstore/inline.spec.ts @@ -10,34 +10,38 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -const fs = require("fs"); -const path = require("path"); +import { test } from "../helpers.ts"; +import { run_standard_tests } from "../helpers/standard_tests.ts"; -/** - * Returns an `ArrayBuffer` containing the contents of a `.arrow` file - * located at `arrow_path`. - * - * Because `fs.readFileSync` shares its underlying buffer - * between calls to `readFileSync`, we need to get a slice - * of the `ArrayBuffer` specifically at its byte offset. - * - * See https://github.com/nodejs/node/issues/11132 for more details. - * - * @param arrow_path {String} a path to an arrow file. - * @returns {ArrayBuffer} an ArrayBuffer containing the arrow-serialized data. - */ -function load_arrow(arrow_path) { - const data = fs.readFileSync(arrow_path); - return data.buffer.slice( - data.byteOffset, - data.byteOffset + data.byteLength, - ); +async function get_contents(page) { + return await page.evaluate(async () => { + const viewer = document.querySelector( + "perspective-viewer perspective-viewer-plugin", + ); + + // Don't format - light DOM is CSV in a
 tag.
+        return viewer.innerHTML;
+    });
 }
 
-const int_float_str_arrow = load_arrow(
-    path.join(__dirname, "..", "arrow", "int_float_str.arrow"),
-);
+test.describe("Superstore Inline", () => {
+    test.beforeEach(async function init({ page }) {
+        await page.goto(
+            "/node_modules/@perspective-dev/viewer/test/html/superstore-inline.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",
+            });
+        });
+    });
 
-module.exports = {
-    int_float_str_arrow,
-};
+    run_standard_tests("superstore inline", get_contents);
+});
diff --git a/rust/perspective-viewer/test/js/superstore.spec.js b/rust/perspective-viewer/test/js/superstore/standard.spec.ts
similarity index 78%
rename from rust/perspective-viewer/test/js/superstore.spec.js
rename to rust/perspective-viewer/test/js/superstore/standard.spec.ts
index fcaba1271d..4a111efaa7 100644
--- a/rust/perspective-viewer/test/js/superstore.spec.js
+++ b/rust/perspective-viewer/test/js/superstore/standard.spec.ts
@@ -10,8 +10,8 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { test } from "@perspective-dev/test";
-import { run_standard_tests } from "@perspective-dev/test";
+import { test } from "../helpers.ts";
+import { run_standard_tests } from "../helpers/standard_tests.ts";
 
 async function get_contents(page) {
     return await page.evaluate(async () => {
@@ -45,25 +45,3 @@ test.describe("Superstore", () => {
 
     run_standard_tests("superstore", get_contents);
 });
-
-test.describe("Superstore inline", () => {
-    test.beforeEach(async function init({ page }) {
-        await page.goto(
-            "/node_modules/@perspective-dev/viewer/test/html/superstore-inline.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",
-            });
-        });
-    });
-
-    run_standard_tests("superstore inline", get_contents);
-});
diff --git a/rust/perspective-viewer/test/js/delete.spec.js b/rust/perspective-viewer/test/js/viewer_api/delete.spec.ts
similarity index 93%
rename from rust/perspective-viewer/test/js/delete.spec.js
rename to rust/perspective-viewer/test/js/viewer_api/delete.spec.ts
index ada31907b1..52575928e2 100644
--- a/rust/perspective-viewer/test/js/delete.spec.js
+++ b/rust/perspective-viewer/test/js/viewer_api/delete.spec.ts
@@ -10,7 +10,7 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { test } from "@perspective-dev/test";
+import { test } from "../helpers.ts";
 
 test.beforeEach(async ({ page }) => {
     await page.goto("/rust/perspective-viewer/test/html/superstore.html");
@@ -28,7 +28,7 @@ test.beforeEach(async ({ page }) => {
 });
 
 test.describe("Delete", async () => {
-    test("Delete shouldn't return until underlying view is deleted", async ({
+    test("delete > blocks until the underlying view is removed", async ({
         page,
     }) => {
         await page.evaluate(async () => {
@@ -46,7 +46,7 @@ test.describe("Delete", async () => {
     });
 
     // `delete()` is destructive now
-    test.skip("Flush after delete should wait for underlying view to be deleted", async ({
+    test.skip("delete > flush after delete waits for view removal", async ({
         page,
     }) => {
         await page.evaluate(async () => {
diff --git a/rust/perspective-viewer/test/js/dom.spec.js b/rust/perspective-viewer/test/js/viewer_api/dom.spec.ts
similarity index 73%
rename from rust/perspective-viewer/test/js/dom.spec.js
rename to rust/perspective-viewer/test/js/viewer_api/dom.spec.ts
index 75955b82d5..716d5c3fd1 100644
--- a/rust/perspective-viewer/test/js/dom.spec.js
+++ b/rust/perspective-viewer/test/js/viewer_api/dom.spec.ts
@@ -10,37 +10,7 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { test, expect } from "@perspective-dev/test";
-import {
-    PageView,
-    compareContentsToSnapshot,
-    shadow_click,
-    shadow_type,
-} from "@perspective-dev/test";
-
-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",
-        });
-    });
-});
+import { test, expect, compareContentsToSnapshot } from "../helpers.ts";
 
 const RESULT = {
     aggregates: {},
@@ -80,14 +50,32 @@ const RESULT = {
     group_rollup_mode: "rollup",
 };
 
+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("DOM API", () => {
-    test.describe("Calling restore() with a table name immediately sets default plugin and columns", () => {
-        test("Proper await order", async ({ page }) => {
+    test.describe("restore with table name > sets default plugin and columns", () => {
+        test("await order > resolves correctly when fully awaited", async ({
+            page,
+        }) => {
             const x = await page.evaluate(async () => {
-                const old = document.querySelector("perspective-viewer");
-                old.parentElement.removeChild(old);
+                const old = document.querySelector("perspective-viewer")!;
+                old.parentElement!.removeChild(old);
                 const viewer = document.createElement("perspective-viewer");
-                await viewer.load(window.__TEST_WORKER__);
+                await viewer.load((window as any).__TEST_WORKER__);
                 await viewer.restore({ table: "load-viewer-csv" });
                 return await viewer.save();
             });
@@ -96,12 +84,14 @@ test.describe("DOM API", () => {
             expect(x).toEqual(RESULT);
         });
 
-        test("Missing await on load()", async ({ page }) => {
+        test("await order > resolves correctly when load is not awaited", async ({
+            page,
+        }) => {
             const x = await page.evaluate(async () => {
-                const old = document.querySelector("perspective-viewer");
-                old.parentElement.removeChild(old);
+                const old = document.querySelector("perspective-viewer")!;
+                old.parentElement!.removeChild(old);
                 const viewer = document.createElement("perspective-viewer");
-                viewer.load(window.__TEST_WORKER__);
+                viewer.load((window as any).__TEST_WORKER__);
                 await viewer.restore({ table: "load-viewer-csv" });
                 return await viewer.save();
             });
@@ -110,12 +100,14 @@ test.describe("DOM API", () => {
             expect(x).toEqual(RESULT);
         });
 
-        test("Missing await on restore()", async ({ page }) => {
+        test("await order > resolves correctly when restore is not awaited", async ({
+            page,
+        }) => {
             const x = await page.evaluate(async () => {
-                const old = document.querySelector("perspective-viewer");
-                old.parentElement.removeChild(old);
+                const old = document.querySelector("perspective-viewer")!;
+                old.parentElement!.removeChild(old);
                 const viewer = document.createElement("perspective-viewer");
-                await viewer.load(window.__TEST_WORKER__);
+                await viewer.load((window as any).__TEST_WORKER__);
                 viewer.restore({ table: "load-viewer-csv" });
                 return await viewer.save();
             });
@@ -124,12 +116,14 @@ test.describe("DOM API", () => {
             expect(x).toEqual(RESULT);
         });
 
-        test("Missing await on both", async ({ page }) => {
+        test("await order > resolves correctly when neither load nor restore is awaited", async ({
+            page,
+        }) => {
             const x = await page.evaluate(async () => {
-                const old = document.querySelector("perspective-viewer");
-                old.parentElement.removeChild(old);
+                const old = document.querySelector("perspective-viewer")!;
+                old.parentElement!.removeChild(old);
                 const viewer = document.createElement("perspective-viewer");
-                viewer.load(window.__TEST_WORKER__);
+                viewer.load((window as any).__TEST_WORKER__);
                 viewer.restore({ table: "load-viewer-csv" });
                 return await viewer.save();
             });
@@ -139,39 +133,39 @@ test.describe("DOM API", () => {
         });
     });
 
-    test.describe("Calling load() and restore() before appending to the DOM", () => {
-        test("Proper await order", async ({ page }) => {
+    test.describe("load and restore before DOM append", () => {
+        test("append > renders correctly when fully awaited", async ({
+            page,
+        }) => {
             const contents = await page.evaluate(async () => {
-                const old = document.querySelector("perspective-viewer");
-                old.parentElement.removeChild(old);
+                const old = document.querySelector("perspective-viewer")!;
+                old.parentElement!.removeChild(old);
                 const viewer = document.createElement("perspective-viewer");
-                await viewer.load(window.__TEST_WORKER__);
+                await viewer.load((window as any).__TEST_WORKER__);
                 await viewer.restore({ table: "load-viewer-csv" });
                 document.body.appendChild(viewer);
                 await viewer.flush();
                 return document.body.innerHTML;
             });
 
-            await compareContentsToSnapshot(contents, [
-                "load-restore-before-append.txt",
-            ]);
+            await compareContentsToSnapshot(contents);
         });
 
-        test("Missing await on restore()", async ({ page }) => {
+        test("append > renders correctly when restore is not awaited", async ({
+            page,
+        }) => {
             const contents = await page.evaluate(async () => {
-                const old = document.querySelector("perspective-viewer");
-                old.parentElement.removeChild(old);
+                const old = document.querySelector("perspective-viewer")!;
+                old.parentElement!.removeChild(old);
                 const viewer = document.createElement("perspective-viewer");
-                await viewer.load(window.__TEST_WORKER__);
+                await viewer.load((window as any).__TEST_WORKER__);
                 viewer.restore({ table: "load-viewer-csv" });
                 document.body.appendChild(viewer);
                 await viewer.flush();
                 return document.body.innerHTML;
             });
 
-            await compareContentsToSnapshot(contents, [
-                "load-restore-before-append.txt",
-            ]);
+            await compareContentsToSnapshot(contents);
         });
     });
 });
diff --git a/rust/perspective-viewer/test/js/events.spec.ts b/rust/perspective-viewer/test/js/viewer_api/events.spec.ts
similarity index 70%
rename from rust/perspective-viewer/test/js/events.spec.ts
rename to rust/perspective-viewer/test/js/viewer_api/events.spec.ts
index 60ef965ee4..ad647a037e 100644
--- a/rust/perspective-viewer/test/js/events.spec.ts
+++ b/rust/perspective-viewer/test/js/viewer_api/events.spec.ts
@@ -10,21 +10,15 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { test, expect, shadow_type } from "@perspective-dev/test";
-import { compareContentsToSnapshot, API_VERSION } from "@perspective-dev/test";
-import * as prettier from "prettier";
-
-async function get_contents(page) {
-    const raw = await page.evaluate(async () => {
-        // @ts-ignore
-        const viewer = document.querySelector("perspective-viewer").shadowRoot;
-        return viewer ? viewer.innerHTML : "MISSING";
-    });
+import {
+    test,
+    expect,
+    compareContentsToSnapshot,
+    API_VERSION,
+    getShadowContents,
+} from "../helpers.ts";
 
-    return await prettier.format(raw, {
-        parser: "html",
-    });
-}
+const get_contents = getShadowContents;
 
 test.beforeEach(async ({ page }) => {
     await page.goto("/rust/perspective-viewer/test/html/superstore.html");
@@ -42,9 +36,7 @@ test.beforeEach(async ({ page }) => {
 });
 
 test.describe("Events", () => {
-    test("restore fires the 'perspective-config-update' event", async ({
-        page,
-    }) => {
+    test("config-update event > fires on restore", async ({ page }) => {
         const config = await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
 
@@ -90,14 +82,10 @@ test.describe("Events", () => {
 
         const contents = await get_contents(page);
 
-        await compareContentsToSnapshot(contents, [
-            "restore-fires-the-perspective-config-update-event.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Editing the title fires the 'perspective-config-update' event", async ({
-        page,
-    }) => {
+    test("config-update event > fires on title edit", async ({ page }) => {
         await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
             window["acc"] = [];
@@ -111,14 +99,10 @@ test.describe("Events", () => {
             });
         });
 
-        await shadow_type(
-            page,
-            "New Title",
-            true,
-            "perspective-viewer",
-            "#status_bar",
-            "input",
-        );
+        const titleInput = page.locator("perspective-viewer #status_bar input");
+        await titleInput.focus();
+        await titleInput.pressSequentially("New Title");
+        await titleInput.blur();
 
         const result = await page.evaluate(async () => {
             return window["acc"];
@@ -133,29 +117,4 @@ test.describe("Events", () => {
 
         expect(config.title).toEqual("New Title");
     });
-
-    // NOTE: Previously skipped, kept for future reference
-    // test.skip("restore with a 'plugin' field fires the 'perspective-plugin-update' event", async ({
-    //     page,
-    // }) => {
-    //     const config = await page.evaluate(async () => {
-    //         const viewer = document.querySelector("perspective-viewer");
-    //         await viewer.getTable();
-    //         let config;
-    //         viewer.addEventListener("perspective-plugin-update", (event) => {
-    //             config = "DID NOT FAIL";
-    //         });
-
-    //         await viewer.restore({
-    //             settings: true,
-    //             plugin: "Debug",
-    //             group_by: ["State"],
-    //         });
-    //         return config;
-    //     });
-
-    //     expect(config).toEqual("Debug");
-
-    //     return await get_contents(page);
-    // });
 });
diff --git a/rust/perspective-viewer/test/js/flush.spec.js b/rust/perspective-viewer/test/js/viewer_api/flush.spec.ts
similarity index 93%
rename from rust/perspective-viewer/test/js/flush.spec.js
rename to rust/perspective-viewer/test/js/viewer_api/flush.spec.ts
index 5053774f4b..7c3c1e429a 100644
--- a/rust/perspective-viewer/test/js/flush.spec.js
+++ b/rust/perspective-viewer/test/js/viewer_api/flush.spec.ts
@@ -10,7 +10,7 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { test, expect } from "@perspective-dev/test";
+import { test, expect } from "../helpers.ts";
 
 test.beforeEach(async ({ page }) => {
     await page.goto("/rust/perspective-viewer/test/html/superstore.html");
@@ -28,7 +28,7 @@ test.beforeEach(async ({ page }) => {
 });
 
 test.describe("Flush method", async () => {
-    test("flush awaits settings view config field", async ({ page }) => {
+    test("flush > awaits settings field update", async ({ page }) => {
         const old_config = await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
             return await viewer.save();
@@ -52,7 +52,7 @@ test.describe("Flush method", async () => {
         });
     });
 
-    test("flush awaits view query fields", async ({ page }) => {
+    test("flush > awaits view query field updates", async ({ page }) => {
         const old_config = await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
             return await viewer.save();
@@ -80,7 +80,7 @@ test.describe("Flush method", async () => {
         });
     });
 
-    test("flush awaits perspective-config-update events trigger by restore", async ({
+    test("flush > awaits config-update event triggered by restore", async ({
         page,
     }) => {
         const result = await page.evaluate(async () => {
@@ -106,7 +106,7 @@ test.describe("Flush method", async () => {
         expect(result).toEqual([0, 1]);
     });
 
-    test("flush awaits perspective-config-update events trigger by load, and does not repeat when connected", async ({
+    test("flush > awaits config-update event on load and does not repeat on DOM connect", async ({
         page,
     }) => {
         const result = await page.evaluate(async () => {
diff --git a/rust/perspective-viewer/test/js/focus.spec.js b/rust/perspective-viewer/test/js/viewer_api/focus.spec.ts
similarity index 93%
rename from rust/perspective-viewer/test/js/focus.spec.js
rename to rust/perspective-viewer/test/js/viewer_api/focus.spec.ts
index 0157321cf8..48167ee0e7 100644
--- a/rust/perspective-viewer/test/js/focus.spec.js
+++ b/rust/perspective-viewer/test/js/viewer_api/focus.spec.ts
@@ -10,9 +10,9 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { test, expect } from "@perspective-dev/test";
+import { test, expect } from "../helpers.ts";
 
-test.describe("browser focus", async () => {
+test.describe("Focus", async () => {
     test.beforeEach(async function init({ page }) {
         await page.goto(
             "/node_modules/@perspective-dev/viewer/test/html/superstore_with_input.html",
@@ -31,7 +31,7 @@ test.describe("browser focus", async () => {
         });
     });
 
-    test("Focus is not lost on external widgets when a restore call takes place", async ({
+    test("focus > preserves external widget focus during restore", async ({
         page,
     }) => {
         const viewer = page.locator("perspective-viewer");
diff --git a/rust/perspective-viewer/test/js/viewer_api/load.spec.ts b/rust/perspective-viewer/test/js/viewer_api/load.spec.ts
new file mode 100644
index 0000000000..2f0da5f380
--- /dev/null
+++ b/rust/perspective-viewer/test/js/viewer_api/load.spec.ts
@@ -0,0 +1,142 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
+// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ 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,
+    compareContentsToSnapshot,
+    getShadowContents,
+} from "../helpers.ts";
+
+const get_contents = getShadowContents;
+
+test.describe("Viewer Load", () => {
+    test("load > resolves with valid Table promise", async ({ page }) => {
+        await page.goto("/rust/perspective-viewer/test/html/blank.html");
+        await page.waitForFunction(() => "WORKER" in window);
+
+        const viewer = page.locator("perspective-viewer");
+        await viewer.evaluate(async (viewer) => {
+            const goodTable = (await window.WORKER).table("a,b,c\n1,2,3");
+            return viewer.load(goodTable);
+        });
+        await expect(viewer).toHaveText(/"a","b","c"/); // column titles
+    });
+
+    test("load > rejects with failed Table promise", async ({ page }) => {
+        await page.goto("/rust/perspective-viewer/test/html/blank.html");
+        await page.waitForFunction(() => "WORKER" in window);
+
+        const viewer = page.locator("perspective-viewer");
+        await expect(
+            viewer.evaluate((viewer) => {
+                const errorTable = Promise.reject(new Error("blimpy"));
+                return viewer.load(errorTable);
+            }),
+        ).rejects.toThrow("blimpy");
+    });
+
+    test("load > recovers after a rejected Table promise", async ({ page }) => {
+        await page.goto("/rust/perspective-viewer/test/html/blank.html");
+        await page.waitForFunction(() => "WORKER" in window);
+
+        const viewer = page.locator("perspective-viewer");
+        const didError = await viewer.evaluate(async (viewer) => {
+            const errorTable = Promise.reject(new Error("blimpy"));
+            const worker = await window.WORKER;
+            let didError = false;
+            try {
+                await viewer.load(errorTable);
+            } catch (e) {
+                if (e.message.includes("blimpy")) {
+                    didError = true;
+                }
+            }
+
+            const goodTable = worker.table("a,b,c\n1,2,3");
+            await viewer.load(goodTable);
+            return didError;
+        });
+        expect(didError).toBe(true);
+    });
+
+    test("load > is inert when called twice with the same Table", async ({
+        page,
+    }) => {
+        await page.goto("/rust/perspective-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",
+            });
+        });
+
+        const contents = await page.evaluate(async () => {
+            const viewer = document.querySelector("perspective-viewer");
+            await viewer.restore({ settings: true });
+            const table = await viewer.getTable();
+            await viewer.load(table);
+            await viewer.flush();
+            return viewer.shadowRoot.innerHTML;
+        });
+
+        await compareContentsToSnapshot(
+            contents,
+            "load-called-twice-with-the-same-table.txt",
+        );
+    });
+
+    test("load > does not throw when restore is called during a failed load", async ({
+        page,
+        consoleLogs,
+    }) => {
+        const errors = [];
+        page.on("pageerror", async (msg) => {
+            errors.push(`${msg.name}::${msg.message}`);
+        });
+
+        await page.goto("/rust/perspective-viewer/test/html/blank.html", {
+            waitUntil: "networkidle",
+        });
+
+        await page.evaluate(async () => {
+            const viewer = document.querySelector("perspective-viewer");
+            viewer.load(
+                new Promise((_, reject) => reject("Intentional Load Error")),
+            );
+            try {
+                await viewer.restore({
+                    settings: true,
+                    plugin: "Debug",
+                });
+            } catch (e) {
+                // We need to catch this error else the `evaluate()` fails.
+                // We need to await the call because we want it to fail
+                // before continuing the test.
+                console.error("Caught error:", e);
+            }
+
+            await new Promise((x) => setTimeout(x, 1000));
+        });
+
+        const contents = await get_contents(page);
+        expect(errors).toEqual([
+            'Error::Failed to construct table from JsValue("Intentional Load Error")',
+        ]);
+        consoleLogs.expectedLogs.push("error", /Intentional Load Error/);
+    });
+});
diff --git a/rust/perspective-viewer/test/js/save_restore.spec.js b/rust/perspective-viewer/test/js/viewer_api/save_restore.spec.ts
similarity index 85%
rename from rust/perspective-viewer/test/js/save_restore.spec.js
rename to rust/perspective-viewer/test/js/viewer_api/save_restore.spec.ts
index 0fcb6cfaa2..ee4093d60a 100644
--- a/rust/perspective-viewer/test/js/save_restore.spec.js
+++ b/rust/perspective-viewer/test/js/viewer_api/save_restore.spec.ts
@@ -10,20 +10,16 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { test, expect, DEFAULT_CONFIG } from "@perspective-dev/test";
-import { compareContentsToSnapshot, API_VERSION } from "@perspective-dev/test";
-import * as prettier from "prettier";
-
-async function get_contents(page) {
-    const raw = await page.evaluate(async () => {
-        const viewer = document.querySelector("perspective-viewer").shadowRoot;
-        return viewer.innerHTML;
-    });
+import {
+    test,
+    expect,
+    DEFAULT_CONFIG,
+    compareContentsToSnapshot,
+    API_VERSION,
+    getShadowContents,
+} from "../helpers.ts";
 
-    return await prettier.format(raw.trim(), {
-        parser: "html",
-    });
-}
+const get_contents = getShadowContents;
 
 test.beforeEach(async ({ page }) => {
     await page.goto("/rust/perspective-viewer/test/html/superstore.html");
@@ -41,7 +37,7 @@ test.beforeEach(async ({ page }) => {
 });
 
 test.describe("Save/Restore", async () => {
-    test("save returns the current config", async ({ page }) => {
+    test("save > returns the current viewer config", async ({ page }) => {
         const config = await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
             await viewer.getTable();
@@ -62,12 +58,10 @@ test.describe("Save/Restore", async () => {
         });
 
         const contents = await get_contents(page);
-        await compareContentsToSnapshot(contents, [
-            "save-returns-current-config.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("restore restores a config from save", async ({ page }) => {
+    test("restore > roundtrips a saved config", async ({ page }) => {
         const config = await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
             await viewer.getTable();
@@ -138,12 +132,10 @@ test.describe("Save/Restore", async () => {
 
         const contents = await get_contents(page);
 
-        await compareContentsToSnapshot(contents, [
-            "restore-restores-config-from-save.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("save/restore works in string format", async ({ page }) => {
+    test("save > roundtrips in string format", async ({ page }) => {
         const config = await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
             await viewer.getTable();
@@ -172,12 +164,10 @@ test.describe("Save/Restore", async () => {
         });
 
         const contents = await get_contents(page);
-        await compareContentsToSnapshot(contents, [
-            "save-restore-works-in-string-format.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("save/restore works in arraybuffer format", async ({ page }) => {
+    test("save > roundtrips in arraybuffer format", async ({ page }) => {
         const config3 = await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
             await viewer.getTable();
@@ -203,8 +193,6 @@ test.describe("Save/Restore", async () => {
         });
 
         const contents = await get_contents(page);
-        await compareContentsToSnapshot(contents, [
-            "save-restore-works-in-arraybuffer-format.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 });
diff --git a/rust/perspective-viewer/test/js/viewer_api/theme.spec.ts b/rust/perspective-viewer/test/js/viewer_api/theme.spec.ts
new file mode 100644
index 0000000000..68a9760ef1
--- /dev/null
+++ b/rust/perspective-viewer/test/js/viewer_api/theme.spec.ts
@@ -0,0 +1,90 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
+// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ 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("/rust/perspective-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 () => {
+        const viewer = document.querySelector("perspective-viewer")!;
+        await viewer.restore({ plugin: "Debug" });
+        // Explicitly register both themes so the memoized theme cache contains
+        // "Pro Dark" even though superstore.html only loads pro.css.
+        await viewer.resetThemes(["Pro Light", "Pro Dark"]);
+    });
+});
+
+test.describe("Theme", () => {
+    test("restore sets the theme attribute on the host element", async ({
+        page,
+    }) => {
+        await page.evaluate(async () => {
+            const viewer = document.querySelector("perspective-viewer")!;
+            await viewer.getTable();
+            await viewer.restore({ theme: "Pro Dark" });
+        });
+
+        const themeAttr = await page.evaluate(() =>
+            document.querySelector("perspective-viewer")!.getAttribute("theme"),
+        );
+
+        expect(themeAttr).toBe("Pro Dark");
+    });
+
+    test("save returns the correct theme after restore", async ({ page }) => {
+        const saved = await page.evaluate(async () => {
+            const viewer = document.querySelector("perspective-viewer")!;
+            await viewer.getTable();
+            await viewer.restore({ theme: "Pro Dark" });
+            return await viewer.save();
+        });
+
+        expect(saved.theme).toBe("Pro Dark");
+    });
+
+    test("theme is preserved across settings panel open/close toggle", async ({
+        page,
+    }) => {
+        // Set theme, then toggle settings open and closed.
+        // This exercises the UpdateSettingsOpen path which must NOT wipe
+        // available_themes from the PresentationProps snapshot.
+        const saved = await page.evaluate(async () => {
+            const viewer = document.querySelector("perspective-viewer")!;
+            await viewer.getTable();
+            await viewer.restore({ theme: "Pro Dark" });
+            await viewer.restore({ settings: true });
+            await viewer.restore({ settings: false });
+            return await viewer.save();
+        });
+
+        expect(saved.theme).toBe("Pro Dark");
+        expect(saved.settings).toBe(false);
+    });
+
+    test("switching theme updates the theme attribute", async ({ page }) => {
+        const themeAttr = await page.evaluate(async () => {
+            const viewer = document.querySelector("perspective-viewer")!;
+            await viewer.getTable();
+            await viewer.restore({ theme: "Pro Dark" });
+            await viewer.restore({ theme: "Pro Light" });
+            return viewer.getAttribute("theme");
+        });
+
+        expect(themeAttr).toBe("Pro Light");
+    });
+});
diff --git a/rust/perspective-viewer/test/js/viewer_api/view_lifecycle.spec.ts b/rust/perspective-viewer/test/js/viewer_api/view_lifecycle.spec.ts
new file mode 100644
index 0000000000..84e8c30fa9
--- /dev/null
+++ b/rust/perspective-viewer/test/js/viewer_api/view_lifecycle.spec.ts
@@ -0,0 +1,91 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
+// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ 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 "../helpers.ts";
+
+test.describe("View Lifecycle", () => {
+    test("conflation > silences view-not-found during rapid restore", async ({
+        page,
+    }) => {
+        await page.goto(
+            "/rust/perspective-viewer/test/html/superstore_lazy_viewer.html",
+        );
+
+        await page.evaluate(async () => {
+            while (!window["__TEST_PERSPECTIVE_READY__"]) {
+                await new Promise((x) => setTimeout(x, 10));
+            }
+        });
+
+        let vnf = false;
+        page.on("console", (msg) => {
+            if (msg.type() === "error") {
+                if (msg.text().includes("View not found")) {
+                    vnf = true;
+                }
+            }
+        });
+
+        await page.evaluate(async () => {
+            const worker = window.__TEST_WORKER__;
+            let resolve;
+            let is_paused = false;
+            const BasePlugin = customElements.get("perspective-viewer-plugin");
+            class PausePlugin extends BasePlugin {
+                get name() {
+                    return "pause-plugin";
+                }
+
+                async draw(view) {
+                    if (is_paused) {
+                        await new Promise((x) => {
+                            resolve = x;
+                        });
+                    }
+
+                    const size = await view.num_rows();
+                    this.textContent = `Rows: ${size}`;
+                }
+            }
+
+            customElements.define("pause-plugin", PausePlugin);
+            const Viewer = customElements.get("perspective-viewer");
+            Viewer.registerPlugin("pause-plugin");
+
+            // use a new viewer because only new viewers get loaded with the registered plugin
+            const viewer = document.createElement("perspective-viewer");
+            document.body.append(viewer);
+            worker.table("a,b,c\n1,2,3", { name: "A" });
+
+            await viewer.load(worker);
+            await viewer.restore({ table: "A", plugin: "pause-plugin" });
+            is_paused = true;
+
+            // Change in 4.1.0 - empty restore now does not render
+            const restore_task = viewer.restore({
+                plugin: "pause-plugin",
+            });
+
+            while (!resolve) {
+                await new Promise((x) => setTimeout(x, 0));
+            }
+
+            await new Promise((x) => setTimeout(x, 0));
+            resolve();
+            resolve = undefined;
+            is_paused = false;
+            await restore_task;
+        });
+
+        expect(vnf).toBeFalsy();
+    });
+});
diff --git a/rust/perspective-viewer/test/js/cancellable.spec.js b/rust/perspective-viewer/test/js/viewer_config/cancellable.spec.ts
similarity index 80%
rename from rust/perspective-viewer/test/js/cancellable.spec.js
rename to rust/perspective-viewer/test/js/viewer_config/cancellable.spec.ts
index 5e0042ef55..1e6dc18d82 100644
--- a/rust/perspective-viewer/test/js/cancellable.spec.js
+++ b/rust/perspective-viewer/test/js/viewer_config/cancellable.spec.ts
@@ -10,20 +10,13 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { test } from "@perspective-dev/test";
-import { compareContentsToSnapshot } from "@perspective-dev/test";
-import * as prettier from "prettier";
+import {
+    test,
+    compareContentsToSnapshot,
+    getShadowContents,
+} from "../helpers.ts";
 
-async function get_contents(page) {
-    const raw = await page.evaluate(async () => {
-        const viewer = document.querySelector("perspective-viewer").shadowRoot;
-        return viewer.innerHTML;
-    });
-
-    return await prettier.format(raw, {
-        parser: "html",
-    });
-}
+const get_contents = getShadowContents;
 
 test.beforeEach(async ({ page }) => {
     await page.goto("/rust/perspective-viewer/test/html/plugin-resize.html");
@@ -34,8 +27,8 @@ test.beforeEach(async ({ page }) => {
     });
 });
 
-test.describe("Cancellable methods", () => {
-    test("Cancellable view methods do not error", async ({ page }) => {
+test.describe("Cancellable Views", () => {
+    test("resize > does not throw after view deletion", async ({ page }) => {
         await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
             await viewer.restore({
@@ -53,8 +46,6 @@ test.describe("Cancellable methods", () => {
         });
 
         const contents = await get_contents(page);
-        await compareContentsToSnapshot(contents, [
-            "regressions-not_in-filter-works-correctly.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 });
diff --git a/rust/perspective-viewer/test/js/load.spec.js b/rust/perspective-viewer/test/js/viewer_config/custom_elements.spec.ts
similarity index 78%
rename from rust/perspective-viewer/test/js/load.spec.js
rename to rust/perspective-viewer/test/js/viewer_config/custom_elements.spec.ts
index 89acbd567a..cbb892ba63 100644
--- a/rust/perspective-viewer/test/js/load.spec.js
+++ b/rust/perspective-viewer/test/js/viewer_config/custom_elements.spec.ts
@@ -10,7 +10,7 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { compareContentsToSnapshot, test } from "@perspective-dev/test";
+import { test, expect } from "../helpers.ts";
 
 test.beforeEach(async ({ page }) => {
     await page.goto("/rust/perspective-viewer/test/html/superstore.html");
@@ -27,22 +27,19 @@ test.beforeEach(async ({ page }) => {
     });
 });
 
-test.describe("Load", async () => {
-    test("Load called twice with the same `Table` should be inert", async ({
+test.describe("Custom Elements", () => {
+    test("registration > registers copy and export menu elements", async ({
         page,
     }) => {
-        const contents = await page.evaluate(async () => {
-            const viewer = document.querySelector("perspective-viewer");
-            await viewer.restore({ settings: true });
-            const table = await viewer.getTable();
-            await viewer.load(table);
-            await viewer.flush();
-            return viewer.shadowRoot.innerHTML;
+        const export_exists = await page.evaluate(async () => {
+            return !!window.customElements.get("perspective-export-menu");
         });
 
-        await compareContentsToSnapshot(
-            contents,
-            "load-called-twice-with-the-same-table.txt",
-        );
+        const copy_exists = await page.evaluate(async () => {
+            return !!window.customElements.get("perspective-copy-menu");
+        });
+
+        expect(export_exists).toBeTruthy();
+        expect(copy_exists).toBeTruthy();
     });
 });
diff --git a/rust/perspective-viewer/test/js/expressions.spec.js b/rust/perspective-viewer/test/js/viewer_config/expressions.spec.ts
similarity index 76%
rename from rust/perspective-viewer/test/js/expressions.spec.js
rename to rust/perspective-viewer/test/js/viewer_config/expressions.spec.ts
index 90bb9e5267..2c4d4772c8 100644
--- a/rust/perspective-viewer/test/js/expressions.spec.js
+++ b/rust/perspective-viewer/test/js/viewer_config/expressions.spec.ts
@@ -10,15 +10,12 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { test, expect } from "@perspective-dev/test";
 import {
+    test,
+    expect,
     PageView,
     compareContentsToSnapshot,
-    shadow_click,
-    shadow_type,
-} from "@perspective-dev/test";
-
-// NOTE: Change this file to be a .ts file.
+} from "../helpers.ts";
 
 async function openSidebarAndScrollToBottom() {
     const elem = document.querySelector("perspective-viewer");
@@ -51,9 +48,7 @@ test.beforeEach(async ({ page }) => {
 });
 
 test.describe("Expressions", () => {
-    test("Click on add column button opens the expression UI.", async ({
-        page,
-    }) => {
+    test("editor > opens on add-column button click", async ({ page }) => {
         await page.evaluate(openSidebarAndScrollToBottom);
 
         await page.waitForFunction(
@@ -63,7 +58,7 @@ test.describe("Expressions", () => {
                     .shadowRoot.querySelector("#add-expression"),
         );
 
-        await shadow_click(page, "perspective-viewer", "#add-expression");
+        await page.locator("perspective-viewer #add-expression").click();
 
         await page.waitForFunction(() => {
             const root = document
@@ -84,12 +79,10 @@ test.describe("Expressions", () => {
 
         await page.evaluate(() => document.activeElement.blur());
 
-        await compareContentsToSnapshot(contents, [
-            "click-on-add-column-button-opens-the-expression-ui.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Close expression editor with button", async ({ page }) => {
+    test("editor > closes on close button click", async ({ page }) => {
         await page.evaluate(openSidebarAndScrollToBottom);
         await page.waitForFunction(
             () =>
@@ -98,7 +91,7 @@ test.describe("Expressions", () => {
                     .shadowRoot.querySelector("#add-expression"),
         );
 
-        await shadow_click(page, "perspective-viewer", "#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;
@@ -116,30 +109,26 @@ test.describe("Expressions", () => {
             );
         });
 
-        await compareContentsToSnapshot(contents, [
-            "close-expression-editor-with-button.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("An expression with unknown symbols should disable the save button", async ({
-        page,
-    }) => {
+    test("validation > disables save for unknown symbols", async ({ page }) => {
         await checkSaveDisabled(page, "abc");
     });
 
-    test("A type-invalid expression should disable the save button", async ({
+    test("validation > disables save for type-invalid expression", async ({
         page,
     }) => {
         await checkSaveDisabled(page, '"Sales" + "Category";');
     });
 
-    test("An expression with invalid input columns should disable the save button", async ({
+    test("validation > disables save for invalid column references", async ({
         page,
     }) => {
         await checkSaveDisabled(page, '"aaaa" + "Sales";');
     });
 
-    test("Should show both aliased and non-aliased expressions in columns", async ({
+    test("columns > shows both aliased and non-aliased expressions", async ({
         page,
     }) => {
         const contents = await page.evaluate(async () => {
@@ -152,13 +141,11 @@ test.describe("Expressions", () => {
             return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
         });
 
-        await compareContentsToSnapshot(contents, [
-            "should-show-both-aliased-and-non-aliased-expressions-in-columns.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
     // No longer relevant as we cannot save a duplicate identifier
-    test.skip("Should overwrite a duplicate expression alias", async ({
+    test.skip("columns > overwrites a duplicate expression alias", async ({
         page,
     }) => {
         let view = new PageView(page);
@@ -171,13 +158,13 @@ test.describe("Expressions", () => {
             return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
         });
 
-        await compareContentsToSnapshot(contents, [
-            "should-overwrite-a-duplicate-expression-alias.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
     // No longer relevant as we cannot save a duplicate identifier
-    test.skip("Should overwrite a duplicate expression", async ({ page }) => {
+    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);
@@ -187,14 +174,10 @@ test.describe("Expressions", () => {
             return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
         });
 
-        await compareContentsToSnapshot(contents, [
-            "should-overwrite-a-duplicate-expression.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Resetting the viewer should delete all expressions", async ({
-        page,
-    }) => {
+    test("reset > deletes all expressions on full reset", async ({ page }) => {
         await page.evaluate(async () => {
             const elem = document.querySelector("perspective-viewer");
             await elem.toggleConfig(true);
@@ -212,14 +195,10 @@ test.describe("Expressions", () => {
             );
         });
 
-        await compareContentsToSnapshot(contents, [
-            "resetting-the-viewer-should-delete-all-expressions.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Resetting the viewer partially should not delete all expressions", async ({
-        page,
-    }) => {
+    test("reset > preserves expressions on partial reset", async ({ page }) => {
         await page.evaluate(async () => {
             const elem = document.querySelector("perspective-viewer");
             await elem.toggleConfig(true);
@@ -234,12 +213,10 @@ test.describe("Expressions", () => {
             return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
         });
 
-        await compareContentsToSnapshot(content, [
-            "resetting-the-viewer-partially-should-not-delete-all-expressions.txt",
-        ]);
+        await compareContentsToSnapshot(content);
     });
 
-    test("Resetting the viewer when expression as in columns field, should delete all expressions", async ({
+    test("reset > deletes expressions used in columns on full reset", async ({
         page,
     }) => {
         await page.evaluate(async () => {
@@ -260,12 +237,10 @@ test.describe("Expressions", () => {
             );
         });
 
-        await compareContentsToSnapshot(contents, [
-            "resetting-the-viewer-when-expression-as-in-columns-field-should-delete-all-expressions.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Resetting the viewer partially when expression as in columns field, should not delete all expressions", async ({
+    test("reset > preserves expressions used in columns on partial reset", async ({
         page,
     }) => {
         await page.evaluate(async () => {
@@ -283,12 +258,10 @@ test.describe("Expressions", () => {
             return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
         });
 
-        await compareContentsToSnapshot(contents, [
-            "resetting-the-viewer-partially-when-expression-as-in-columns-field-should-not-delete-all-expressions.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Resetting the viewer when expression as in group_by or other field, should delete all expressions", async ({
+    test("reset > deletes expressions used in group_by/sort/filter on full reset", async ({
         page,
     }) => {
         await page.evaluate(async () => {
@@ -312,12 +285,10 @@ test.describe("Expressions", () => {
             );
         });
 
-        await compareContentsToSnapshot(contents, [
-            "resetting-the-viewer-when-expression-as-in-group_by-or-other-field-should-delete-all-expressions.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Expressions should persist when new views are created which don't use them", async ({
+    test("persistence > survives views that don't reference them", async ({
         page,
     }) => {
         await page.evaluate(async () => {
@@ -336,12 +307,10 @@ test.describe("Expressions", () => {
             return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
         });
 
-        await compareContentsToSnapshot(contents, [
-            "expressions-should-persist-when-new-views-are-created-which-dont-use-them.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Hovering over New Expression Button marks it.", async ({ page }) => {
+    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");
@@ -353,7 +322,7 @@ test.describe("Expressions", () => {
     });
 
     // Currently does not work in Firefox!
-    test("Clicking on New Expression Button marks it.", async ({ page }) => {
+    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");
@@ -364,7 +333,7 @@ test.describe("Expressions", () => {
         expect(clicked).toBe("dragdrop-hover");
     });
 
-    test("Expressions should persist when new views are created using them", async ({
+    test("persistence > survives views that reference them", async ({
         page,
     }) => {
         await page.evaluate(async () => {
@@ -383,12 +352,12 @@ test.describe("Expressions", () => {
             return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
         });
 
-        await compareContentsToSnapshot(contents, [
-            "expressions-should-persist-when-new-views-are-created-using-them.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Aggregates for expressions should apply", async ({ page }) => {
+    test("aggregates > applies aggregate to expression column", async ({
+        page,
+    }) => {
         await page.evaluate(async () => {
             const elem = document.querySelector("perspective-viewer");
             await elem.toggleConfig(true);
@@ -405,12 +374,10 @@ test.describe("Expressions", () => {
             return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
         });
 
-        await compareContentsToSnapshot(contents, [
-            "aggregates-for-expressions-should-apply.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Should sort by hidden expressions", async ({ page }) => {
+    test("sort > sorts by a hidden expression column", async ({ page }) => {
         await page.evaluate(async () => {
             const elem = document.querySelector("perspective-viewer");
             await elem.toggleConfig(true);
@@ -426,12 +393,10 @@ test.describe("Expressions", () => {
             return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
         });
 
-        await compareContentsToSnapshot(contents, [
-            "should-sort-by-hidden-expressions.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Should filter by an expression", async ({ page }) => {
+    test("filter > filters by an expression column", async ({ page }) => {
         await page.evaluate(async () => {
             const elem = document.querySelector("perspective-viewer");
             await elem.toggleConfig(true);
@@ -447,8 +412,6 @@ test.describe("Expressions", () => {
             return elem.shadowRoot.querySelector("#sub-columns").innerHTML;
         });
 
-        await compareContentsToSnapshot(contents, [
-            "should-filter-by-an-expression.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 });
diff --git a/rust/perspective-viewer/test/js/regressions.spec.js b/rust/perspective-viewer/test/js/viewer_config/filter.spec.ts
similarity index 73%
rename from rust/perspective-viewer/test/js/regressions.spec.js
rename to rust/perspective-viewer/test/js/viewer_config/filter.spec.ts
index 0ff1ae3c2a..4790f5fa67 100644
--- a/rust/perspective-viewer/test/js/regressions.spec.js
+++ b/rust/perspective-viewer/test/js/viewer_config/filter.spec.ts
@@ -10,24 +10,15 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { test, expect, DEFAULT_CONFIG } from "@perspective-dev/test";
 import {
-    API_VERSION,
+    test,
+    expect,
+    DEFAULT_CONFIG,
     compareContentsToSnapshot,
-    shadow_type,
-} from "@perspective-dev/test";
-import * as prettier from "prettier";
-
-async function get_contents(page) {
-    const raw = await page.evaluate(async () => {
-        const viewer = document.querySelector("perspective-viewer").shadowRoot;
-        return viewer.innerHTML;
-    });
+    getShadowContents,
+} from "../helpers.ts";
 
-    return await prettier.format(raw, {
-        parser: "html",
-    });
-}
+const get_contents = getShadowContents;
 
 test.beforeEach(async ({ page }) => {
     await page.goto("/rust/perspective-viewer/test/html/superstore.html");
@@ -44,21 +35,8 @@ test.beforeEach(async ({ page }) => {
     });
 });
 
-test.describe("Regression tests", () => {
-    test("copy and export custom elements are registered", async ({ page }) => {
-        const export_exists = await page.evaluate(async () => {
-            return !!window.customElements.get("perspective-export-menu");
-        });
-
-        const copy_exists = await page.evaluate(async () => {
-            return !!window.customElements.get("perspective-copy-menu");
-        });
-
-        expect(export_exists).toBeTruthy();
-        expect(copy_exists).toBeTruthy();
-    });
-
-    test("not_in filter works correctly", async ({ page }) => {
+test.describe("Filter Config", () => {
+    test("not_in > renders filtered results correctly", async ({ page }) => {
         await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
             await viewer.restore({
@@ -73,12 +51,10 @@ test.describe("Regression tests", () => {
 
         const contents = await get_contents(page);
 
-        await compareContentsToSnapshot(contents, [
-            "regressions-not_in-filter-works-correctly.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("in filter generates correct array-encoded config", async ({
+    test("in > generates correct config from dropdown selection", async ({
         page,
     }) => {
         await page.evaluate(async () => {
@@ -126,12 +102,10 @@ test.describe("Regression tests", () => {
         });
 
         const contents = await get_contents(page);
-        await compareContentsToSnapshot(contents, [
-            "regressions-in-filter-generates-correct-config.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 
-    test("Numeric filter input does not trigger render on trailing zeroes", async ({
+    test("numeric input > appends digits without re-rendering on trailing zeroes", async ({
         page,
     }) => {
         await page.evaluate(async () => {
@@ -142,15 +116,10 @@ test.describe("Regression tests", () => {
             });
         });
 
-        // await new Promise((x) => setTimeout(x, 10000));
-
-        await shadow_type(
-            page,
-            "0001",
-            true,
-            "perspective-viewer",
-            "input.num-filter",
-        );
+        const numFilter = page.locator("perspective-viewer input.num-filter");
+        await numFilter.focus();
+        await numFilter.pressSequentially("0001");
+        await numFilter.blur();
 
         const value = await page.evaluate(async () => {
             const viewer = document.querySelector("perspective-viewer");
@@ -160,8 +129,6 @@ test.describe("Regression tests", () => {
 
         expect(value).toEqual("1.10001");
         const contents = await get_contents(page);
-        await compareContentsToSnapshot(contents, [
-            "numeric-filter-input-does-not-trigger-render-on-trailing-zeroes.txt",
-        ]);
+        await compareContentsToSnapshot(contents);
     });
 });
diff --git a/rust/perspective-viewer/test/js/intl.spec.js b/rust/perspective-viewer/test/js/viewer_config/localization.spec.ts
similarity index 87%
rename from rust/perspective-viewer/test/js/intl.spec.js
rename to rust/perspective-viewer/test/js/viewer_config/localization.spec.ts
index 109330f461..29c2d5babb 100644
--- a/rust/perspective-viewer/test/js/intl.spec.js
+++ b/rust/perspective-viewer/test/js/viewer_config/localization.spec.ts
@@ -10,8 +10,7 @@
 // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 
-import { PageView as PspViewer } from "@perspective-dev/test";
-import { expect, test } from "@perspective-dev/test";
+import { PageView as PspViewer, expect, test } from "../helpers.ts";
 import fs from "node:fs";
 import { fileURLToPath } from "url";
 import path from "path";
@@ -28,7 +27,7 @@ test.describe("Localization", function () {
         });
     });
 
-    test("All label tags are empty", async function ({ page }) {
+    test("labels > renders no visible label text", async function ({ page }) {
         const view = new PspViewer(page);
         await view.openSettingsPanel();
         const editBtn = view.dataGrid.regularTable.editBtnRow
@@ -48,15 +47,17 @@ test.describe("Localization", function () {
     });
 
     const intl = fs
-        .readFileSync(`${__dirname}/../../src/themes/intl.less`)
+        .readFileSync(`${__dirname}/../../../src/themes/intl.less`)
         .toString();
 
     const keys = Array.from(intl.matchAll(/--[a-zA-Z0-9\-]+/g)).flat();
-    const langfiles = fs.readdirSync(`${__dirname}/../../src/themes/intl`);
+    const langfiles = fs.readdirSync(`${__dirname}/../../../src/themes/intl`);
     for (const file of langfiles) {
-        test(`${file} has all intl keys present`, async function ({ page }) {
+        test(`${file} > contains all required intl keys`, async function ({
+            page,
+        }) {
             const langfile = fs
-                .readFileSync(`${__dirname}/../../src/themes/intl/${file}`)
+                .readFileSync(`${__dirname}/../../../src/themes/intl/${file}`)
                 .toString();
             for (const key of keys) {
                 const re = new RegExp(key, "g");
diff --git a/rust/perspective-viewer/test/js/viewnotfound.spec.js b/rust/perspective-viewer/test/js/viewnotfound.spec.js
deleted file mode 100644
index 544f5ee004..0000000000
--- a/rust/perspective-viewer/test/js/viewnotfound.spec.js
+++ /dev/null
@@ -1,87 +0,0 @@
-// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
-// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
-// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
-// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
-// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
-// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
-// ┃ 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("View conflation is silenced", async ({ page }) => {
-    await page.goto(
-        "/rust/perspective-viewer/test/html/superstore_lazy_viewer.html",
-    );
-
-    await page.evaluate(async () => {
-        while (!window["__TEST_PERSPECTIVE_READY__"]) {
-            await new Promise((x) => setTimeout(x, 10));
-        }
-    });
-
-    let vnf = false;
-    page.on("console", (msg) => {
-        if (msg.type() === "error") {
-            if (msg.text().includes("View not found")) {
-                vnf = true;
-            }
-        }
-    });
-
-    await page.evaluate(async () => {
-        const worker = window.__TEST_WORKER__;
-        let resolve;
-        let is_paused = false;
-        const BasePlugin = customElements.get("perspective-viewer-plugin");
-        class PausePlugin extends BasePlugin {
-            get name() {
-                return "pause-plugin";
-            }
-
-            async draw(view) {
-                if (is_paused) {
-                    await new Promise((x) => {
-                        resolve = x;
-                    });
-                }
-
-                const size = await view.num_rows();
-                this.textContent = `Rows: ${size}`;
-            }
-        }
-
-        customElements.define("pause-plugin", PausePlugin);
-        const Viewer = customElements.get("perspective-viewer");
-        Viewer.registerPlugin("pause-plugin");
-
-        // use a new viewer because only new viewers get loaded with the registered plugin
-        const viewer = document.createElement("perspective-viewer");
-        document.body.append(viewer);
-        worker.table("a,b,c\n1,2,3", { name: "A" });
-
-        await viewer.load(worker);
-        await viewer.restore({ table: "A", plugin: "pause-plugin" });
-        is_paused = true;
-
-        // Change in 4.1.0 - empty restore now does not render
-        const restore_task = viewer.restore({
-            plugin: "pause-plugin",
-        });
-
-        while (!resolve) {
-            await new Promise((x) => setTimeout(x, 0));
-        }
-
-        await new Promise((x) => setTimeout(x, 0));
-        resolve();
-        resolve = undefined;
-        is_paused = false;
-        await restore_task;
-    });
-
-    expect(vnf).toBeFalsy();
-});
diff --git a/tools/test/playwright.config.ts b/tools/test/playwright.config.ts
index 187b5e18af..e00fa32737 100644
--- a/tools/test/playwright.config.ts
+++ b/tools/test/playwright.config.ts
@@ -199,7 +199,9 @@ export default defineConfig({
     expect: {
         timeout: 30_000,
     },
-    repeatEach: process.env.PSP_SATURATE ? parseInt(process.env.PSP_SATURATE) : 0,
+    repeatEach: process.env.PSP_SATURATE
+        ? parseInt(process.env.PSP_SATURATE)
+        : 0,
     forbidOnly: !!process.env.CI,
     workers: process.env.PSP_DEBUG ? 1 : "50%",
     retries: 0,
@@ -208,6 +210,9 @@ export default defineConfig({
     projects: PROJECTS,
     outputDir: "dist/results",
     use: {
+        launchOptions: {
+            slowMo: 500, // 500 milliseconds delay
+        },
         headless: !process.env.PSP_HEADED,
         ctPort: 3100,
         viewport: { width: 1280, height: 720 },
diff --git a/tools/test/results.tar.gz b/tools/test/results.tar.gz
index 564106469f1a813f615e43ade11d1c65839ef862..bb24cb7a98d7df44d9a10d7986a173b018a96e9a 100644
GIT binary patch
literal 171912
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%0DQ1eE@Y@Xr^EMZ#pMPk#{B2gS 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%0AXg_+chD5nb<@t8~GUEMIq0NZ3$+p$-fQa7Yt-C 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 SPN?7&M1eukAM2R{)_(z~w5m@4 literal 159829 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 async (page: Page) => { */ export async function compareContentsToSnapshot( contents: string, - snapshotPath: string[], ): Promise { const cleanedContents = contents .replace(/style=""/g, "") @@ -169,16 +168,28 @@ export async function compareContentsToSnapshot( parser: "html", }); + const titlePath = test.info().titlePath; + const snapshotPath = [ + titlePath + .slice(1) + .map((s) => + s + .trim() + .replace(/[^a-z0-9]+/gi, "-") + .toLowerCase(), + ) + .join("-") + ".txt", + ]; + await expect(formatted).toMatchSnapshot(snapshotPath); } export async function compareSVGContentsToSnapshot( page: Page, selector: string, - snapshotPath: string[], ): Promise { const svgContent = await getSvgContentString(selector)(page); - await compareContentsToSnapshot(svgContent, snapshotPath); + await compareContentsToSnapshot(svgContent); } export function getWorkspaceLightDOMContents(page: Page): Promise { @@ -195,14 +206,14 @@ export function getWorkspaceShadowDOMContents(page: Page): Promise { }); } -export async function compareLightDOMContents(page, snapshotFileName) { +export async function compareLightDOMContents(page) { const contents = await getWorkspaceLightDOMContents(page); - await compareContentsToSnapshot(contents, [snapshotFileName]); + await compareContentsToSnapshot(contents); } -export async function compareShadowDOMContents(page, snapshotFileName) { +export async function compareShadowDOMContents(page) { const contents = await getWorkspaceShadowDOMContents(page); - await compareContentsToSnapshot(contents, [snapshotFileName]); + await compareContentsToSnapshot(contents); } /** From 7aa374abef3233dae74bfcfc52f968a72b36a2c0 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 15 Mar 2026 23:30:15 -0400 Subject: [PATCH 2/3] Refactor viewer state model Signed-off-by: Andrew Stein --- examples/blocks/src/fractal/index.js | 16 +- .../src/ts/style_handlers/body.ts | 26 +- rust/perspective-client/src/rust/client.rs | 2 +- rust/perspective-client/src/rust/table.rs | 2 +- .../src/rust/virtual_server/features.rs | 4 +- rust/perspective-js/src/rust/utils/futures.rs | 4 + .../src/rust/components/column_selector.rs | 219 +++++---- .../column_selector/active_column.rs | 127 +++--- .../column_selector/aggregate_selector.rs | 39 +- .../column_selector/config_selector.rs | 332 +++++++++----- .../column_selector/filter_column.rs | 60 +-- .../column_selector/inactive_column.rs | 56 ++- .../column_selector/invalid_column.rs | 25 +- .../column_selector/pivot_column.rs | 17 +- .../components/column_selector/sort_column.rs | 37 +- .../components/column_settings_sidebar.rs | 137 +++--- .../column_settings_sidebar/style_tab.rs | 262 ++++++----- .../src/rust/components/copy_dropdown.rs | 14 +- .../rust/components/datetime_column_style.rs | 6 +- .../datetime_column_style/custom.rs | 4 +- .../datetime_column_style/simple.rs | 4 +- .../src/rust/components/editable_header.rs | 11 +- .../src/rust/components/export_dropdown.rs | 11 +- .../src/rust/components/expression_editor.rs | 20 +- .../src/rust/components/font_loader.rs | 39 +- .../src/rust/components/form/debug.rs | 43 +- .../src/rust/components/main_panel.rs | 160 +++---- .../src/rust/components/mod.rs | 1 - .../rust/components/number_column_style.rs | 6 +- .../src/rust/components/plugin_selector.rs | 130 ++---- .../src/rust/components/render_warning.rs | 195 +++----- .../src/rust/components/settings_panel.rs | 119 ++++- .../src/rust/components/status_bar.rs | 200 +++++---- .../src/rust/components/status_bar_counter.rs | 28 +- .../src/rust/components/status_indicator.rs | 187 +++----- .../rust/components/string_column_style.rs | 4 +- .../src/rust/components/viewer.rs | 425 ++++++++++++++++-- .../src/rust/custom_elements/copy_dropdown.rs | 2 +- .../rust/custom_elements/export_dropdown.rs | 2 +- .../src/rust/custom_elements/viewer.rs | 56 ++- .../src/rust/custom_events.rs | 21 +- rust/perspective-viewer/src/rust/dragdrop.rs | 38 +- rust/perspective-viewer/src/rust/engines.rs | 28 ++ rust/perspective-viewer/src/rust/js/plugin.rs | 2 +- rust/perspective-viewer/src/rust/lib.rs | 6 +- .../src/rust/model/column_locator.rs | 82 ---- .../src/rust/model/structural.rs | 244 ---------- .../src/rust/presentation.rs | 44 +- .../src/rust/presentation/props.rs | 40 ++ rust/perspective-viewer/src/rust/renderer.rs | 67 ++- .../src/rust/renderer/limits.rs | 35 +- .../{model/reset_all.rs => renderer/props.rs} | 45 +- rust/perspective-viewer/src/rust/session.rs | 191 ++++---- .../src/rust/session/metadata.rs | 22 +- .../src/rust/session/props.rs | 172 +++++++ .../src/rust/tasks/column_locator.rs | 133 ++++++ .../rust/{model => tasks}/columns_iter_set.rs | 37 +- .../src/rust/{model => tasks}/copy_export.rs | 0 .../rust/{model => tasks}/edit_expression.rs | 24 +- .../src/rust/{model => tasks}/eject.rs | 0 .../src/rust/{model => tasks}/export_app.rs | 0 .../rust/{model => tasks}/export_method.rs | 0 .../{model => tasks}/get_viewer_config.rs | 11 - .../{model => tasks}/intersection_observer.rs | 22 +- .../rust/{model => tasks}/is_invalid_drop.rs | 42 +- .../src/rust/{model => tasks}/mod.rs | 62 +-- .../{model => tasks}/plugin_column_styles.rs | 115 +++-- .../rust/{model => tasks}/resize_observer.rs | 38 +- .../{model => tasks}/restore_and_render.rs | 0 .../{model => tasks}/send_plugin_config.rs | 2 +- .../error_message.rs => tasks/structural.rs} | 77 ++-- .../{model => tasks}/update_and_render.rs | 0 .../src/rust/utils/pubsub.rs | 8 + 73 files changed, 2740 insertions(+), 1900 deletions(-) create mode 100644 rust/perspective-viewer/src/rust/engines.rs delete mode 100644 rust/perspective-viewer/src/rust/model/column_locator.rs delete mode 100644 rust/perspective-viewer/src/rust/model/structural.rs create mode 100644 rust/perspective-viewer/src/rust/presentation/props.rs rename rust/perspective-viewer/src/rust/{model/reset_all.rs => renderer/props.rs} (69%) create mode 100644 rust/perspective-viewer/src/rust/session/props.rs create mode 100644 rust/perspective-viewer/src/rust/tasks/column_locator.rs rename rust/perspective-viewer/src/rust/{model => tasks}/columns_iter_set.rs (94%) rename rust/perspective-viewer/src/rust/{model => tasks}/copy_export.rs (100%) rename rust/perspective-viewer/src/rust/{model => tasks}/edit_expression.rs (91%) rename rust/perspective-viewer/src/rust/{model => tasks}/eject.rs (100%) rename rust/perspective-viewer/src/rust/{model => tasks}/export_app.rs (100%) rename rust/perspective-viewer/src/rust/{model => tasks}/export_method.rs (100%) rename rust/perspective-viewer/src/rust/{model => tasks}/get_viewer_config.rs (89%) rename rust/perspective-viewer/src/rust/{model => tasks}/intersection_observer.rs (91%) rename rust/perspective-viewer/src/rust/{model => tasks}/is_invalid_drop.rs (70%) rename rust/perspective-viewer/src/rust/{model => tasks}/mod.rs (58%) rename rust/perspective-viewer/src/rust/{model => tasks}/plugin_column_styles.rs (50%) rename rust/perspective-viewer/src/rust/{model => tasks}/resize_observer.rs (85%) rename rust/perspective-viewer/src/rust/{model => tasks}/restore_and_render.rs (100%) rename rust/perspective-viewer/src/rust/{model => tasks}/send_plugin_config.rs (99%) rename rust/perspective-viewer/src/rust/{components/error_message.rs => tasks/structural.rs} (60%) rename rust/perspective-viewer/src/rust/{model => tasks}/update_and_render.rs (100%) diff --git a/examples/blocks/src/fractal/index.js b/examples/blocks/src/fractal/index.js index 4ff4b48e8d..01309ac6ec 100644 --- a/examples/blocks/src/fractal/index.js +++ b/examples/blocks/src/fractal/index.js @@ -56,6 +56,7 @@ c`; function generate_layout(params) { return { plugin: "Heatmap", + table: "raw_data", settings: true, group_by: [`floor("index" / ${params.resolution})`], split_by: [`"index" % ${params.resolution}`], @@ -116,10 +117,16 @@ const make_run_click_callback = (worker, state) => async () => { window.run.disabled = true; if (!state.table) { - state.table = await worker.table({ - index: "integer", - }); - window.viewer.load(Promise.resolve(state.table)); + state.table = await worker.table( + { + index: "integer", + }, + { + name: "raw_data", + }, + ); + + window.viewer.load(worker); } const run = document.getElementById("run"); @@ -154,4 +161,5 @@ run.addEventListener( "click", make_run_click_callback(await perspective.worker(), {}), ); + run.dispatchEvent(new Event("click")); diff --git a/packages/viewer-datagrid/src/ts/style_handlers/body.ts b/packages/viewer-datagrid/src/ts/style_handlers/body.ts index c5b0d614e2..97462ae06e 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/body.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/body.ts @@ -65,18 +65,20 @@ export function applyBodyCellStyles( // Calculate aggregate depth visibility // @ts-ignore - metadata._is_hidden_by_aggregate_depth = ((x?: number) => - x === 0 || x === undefined - ? false - : x - 1 < - Math.min( - this._config.group_by.length, - plugin?.aggregate_depth || 0, - ))( - (metadata.row_header as unknown[] | undefined)?.filter( - (x) => x !== undefined, - )?.length, - ); + metadata._is_hidden_by_aggregate_depth = + this._config.group_rollup_mode === "rollup" && + ((x?: number) => + x === 0 || x === undefined + ? false + : x - 1 < + Math.min( + this._config.group_by.length, + plugin?.aggregate_depth || 0, + ))( + (metadata.row_header as unknown[] | undefined)?.filter( + (x) => x !== undefined, + )?.length, + ); // Apply type-specific cell styling if (is_numeric) { diff --git a/rust/perspective-client/src/rust/client.rs b/rust/perspective-client/src/rust/client.rs index 53a443ffa1..041c9edcc3 100644 --- a/rust/perspective-client/src/rust/client.rs +++ b/rust/perspective-client/src/rust/client.rs @@ -88,7 +88,7 @@ impl SystemInfo { /// Metadata about what features are supported by the `Server` to which this /// [`Client`] connects. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Features(Arc); impl Features { diff --git a/rust/perspective-client/src/rust/table.rs b/rust/perspective-client/src/rust/table.rs index 4070900383..f499e6d9e5 100644 --- a/rust/perspective-client/src/rust/table.rs +++ b/rust/perspective-client/src/rust/table.rs @@ -152,7 +152,7 @@ pub struct UpdateOptions { /// Result of a call to [`Table::validate_expressions`], containing a schema /// for valid expressions and error messages for invalid ones. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ExprValidationResult { pub expression_schema: Schema, pub errors: HashMap, diff --git a/rust/perspective-client/src/rust/virtual_server/features.rs b/rust/perspective-client/src/rust/virtual_server/features.rs index 97e1e952ad..03399dd556 100644 --- a/rust/perspective-client/src/rust/virtual_server/features.rs +++ b/rust/perspective-client/src/rust/virtual_server/features.rs @@ -24,7 +24,7 @@ use crate::proto::{ColumnType, GetFeaturesResp}; /// This struct is returned by /// [`VirtualServerHandler::get_features`](super::VirtualServerHandler::get_features) /// to inform clients about which operations are available. -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct Features<'a> { /// Whether group-by aggregation is supported. #[serde(default)] @@ -63,7 +63,7 @@ pub struct Features<'a> { /// /// Aggregates can either take no additional arguments ([`AggSpec::Single`]) /// or require column type arguments ([`AggSpec::Multiple`]). -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(untagged)] pub enum AggSpec<'a> { /// An aggregate function with no additional arguments. diff --git a/rust/perspective-js/src/rust/utils/futures.rs b/rust/perspective-js/src/rust/utils/futures.rs index 7b8beed3c6..50fc6001d1 100644 --- a/rust/perspective-js/src/rust/utils/futures.rs +++ b/rust/perspective-js/src/rust/utils/futures.rs @@ -64,6 +64,10 @@ where pub fn spawn> + 'static>(x: U) { drop(js_sys::Promise::from(Self::new(x))) } + + pub fn spawn_throttled> + 'static>(x: U) { + drop(js_sys::Promise::from(Self::new_throttled(x))) + } } impl Default for ApiFuture diff --git a/rust/perspective-viewer/src/rust/components/column_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector.rs index 6911442bff..508dab014b 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector.rs @@ -40,16 +40,19 @@ use super::containers::scroll_panel::*; use super::containers::split_panel::{Orientation, SplitPanel}; use super::style::LocalStyle; use crate::components::containers::scroll_panel_item::ScrollPanelItem; +use crate::css; use crate::custom_elements::ColumnDropDownElement; use crate::dragdrop::*; -use crate::model::*; use crate::presentation::ColumnLocator; use crate::renderer::*; +use crate::session::drag_drop_update::*; use crate::session::*; +use crate::tasks::{ + ActiveColumnState, ActiveColumnStateData, ColumnsIteratorSet, can_render_column_styles, +}; use crate::utils::*; -use crate::*; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Properties)] pub struct ColumnSelectorProps { /// Fires when the expression/config column is open. pub on_open_expr_panel: Callback, @@ -61,6 +64,15 @@ pub struct ColumnSelectorProps { #[prop_or_default] pub on_resize: Option>>, + /// Value props threaded from root's `SessionProps` / `RendererProps`. + pub has_table: bool, + pub named_column_count: usize, + pub view_config: Rc, + pub drag_column: Option, + /// Cloned session metadata snapshot — threaded from `SessionProps` + /// so that metadata changes trigger re-renders via prop diffing. + pub metadata: Rc, + // State pub session: Session, pub renderer: Renderer, @@ -70,17 +82,21 @@ pub struct ColumnSelectorProps { impl PartialEq for ColumnSelectorProps { fn eq(&self, rhs: &Self) -> bool { self.selected_column == rhs.selected_column + && self.has_table == rhs.has_table + && self.named_column_count == rhs.named_column_count + && self.view_config == rhs.view_config + && self.drag_column == rhs.drag_column + && self.metadata == rhs.metadata } } #[derive(Debug)] pub enum ColumnSelectorMsg { - TableLoaded, - ViewCreated, + /// Triggers a plain re-render; used as `onselect`/`ondragenter` callbacks + /// from `ConfigSelector` after it mutates the view config. + Redraw, HoverActiveIndex(Option), SetWidth(f64), - Drag(DragEffect), - DragEnd, Drop((String, DragTarget, DragEffect, usize)), } @@ -89,8 +105,7 @@ use ColumnSelectorMsg::*; /// A `ColumnSelector` controls the `columns` field of the `ViewConfig`, /// deriving its options from the table columns and `ViewConfig` expressions. pub struct ColumnSelector { - _subscriptions: [Subscription; 5], - named_row_count: usize, + _subscriptions: [Subscription; 1], drag_container: DragDropContainer, column_dropdown: ColumnDropDownElement, viewport_width: f64, @@ -103,44 +118,14 @@ impl Component for ColumnSelector { fn create(ctx: &Context) -> Self { let ColumnSelectorProps { - dragdrop, - renderer, - session, - .. + dragdrop, session, .. } = ctx.props(); - let table_sub = { - let cb = ctx.link().callback(|_| ColumnSelectorMsg::TableLoaded); - session.table_loaded.add_listener(cb) - }; - - let view_sub = { - let cb = ctx.link().callback(|_| ColumnSelectorMsg::ViewCreated); - session.view_created.add_listener(cb) - }; let drop_sub = { let cb = ctx.link().callback(ColumnSelectorMsg::Drop); dragdrop.drop_received.add_listener(cb) }; - let drag_sub = { - let cb = ctx.link().callback(ColumnSelectorMsg::Drag); - dragdrop.dragstart_received.add_listener(cb) - }; - - let dragend_sub = { - let cb = ctx.link().callback(|_| ColumnSelectorMsg::DragEnd); - dragdrop.dragend_received.add_listener(cb) - }; - - let named = maybe! { - let plugin = - renderer.get_active_plugin().ok()?; - - Some(plugin.config_column_names()?.length() as usize) - }; - - let named_row_count = named.unwrap_or_default(); let drag_container = DragDropContainer::new(|| {}, { let link = ctx.link().clone(); move || link.send_message(ColumnSelectorMsg::HoverActiveIndex(None)) @@ -148,8 +133,7 @@ impl Component for ColumnSelector { let column_dropdown = ColumnDropDownElement::new(session.clone()); Self { - _subscriptions: [table_sub, view_sub, drop_sub, drag_sub, dragend_sub], - named_row_count, + _subscriptions: [drop_sub], viewport_width: 0f64, drag_container, column_dropdown, @@ -159,23 +143,11 @@ impl Component for ColumnSelector { fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - Drag(DragEffect::Move(DragTarget::Active)) => false, - Drag(_) | DragEnd | TableLoaded => true, + Redraw => true, SetWidth(w) => { self.viewport_width = w; false }, - ViewCreated => { - let named = maybe! { - let plugin = - ctx.props().renderer.get_active_plugin().ok()?; - - Some(plugin.config_column_names()?.length() as usize) - }; - - self.named_row_count = named.unwrap_or_default(); - true - }, HoverActiveIndex(Some(to_index)) => ctx .props() .dragdrop @@ -185,33 +157,74 @@ impl Component for ColumnSelector { true }, Drop((column, DragTarget::Active, DragEffect::Move(DragTarget::Active), index)) => { - if !ctx.props().is_invalid_columns_column(&column, index) { - let update = ctx.props().session.create_drag_drop_update( + let is_invalid = { + let config = &ctx.props().view_config; + let from_index = config + .columns + .iter() + .position(|x| x.as_ref() == Some(&column)); + let min_cols = ctx.props().renderer.metadata().min; + let is_to_empty = !config + .columns + .get(index) + .map(|x| x.is_some()) + .unwrap_or_default(); + min_cols + .and_then(|x| from_index.map(|fi| fi < x)) + .unwrap_or_default() + && is_to_empty + }; + if !is_invalid { + let col_type = ctx + .props() + .metadata + .get_column_table_type(column.as_str()) + .unwrap(); + let update = ctx.props().view_config.create_drag_drop_update( column, + col_type, index, DragTarget::Active, DragEffect::Move(DragTarget::Active), &ctx.props().renderer.metadata(), + ctx.props().metadata.get_features().unwrap(), ); - if let Ok(task) = ctx.props().update_and_render(update) { - ApiFuture::spawn(task); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); } } true }, Drop((column, DragTarget::Active, effect, index)) => { - let update = ctx.props().session.create_drag_drop_update( + let col_type = ctx + .props() + .metadata + .get_column_table_type(column.as_str()) + .unwrap(); + let update = ctx.props().view_config.create_drag_drop_update( column, + col_type, index, DragTarget::Active, effect, &ctx.props().renderer.metadata(), + ctx.props().metadata.get_features().unwrap(), ); - if let Ok(task) = ctx.props().update_and_render(update) { - ApiFuture::spawn(task); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); } true @@ -228,10 +241,26 @@ impl Component for ColumnSelector { dragdrop, .. } = ctx.props(); - let config = session.get_view_config(); + let metadata = &ctx.props().metadata; + // When `config.columns` is empty but the table has columns (transient + // state during `load()` after `reset()` clears the config), fill in + // all table columns as active — matching `validate_view_config()`. + let prop_config = &ctx.props().view_config; + let config: Rc = if prop_config.columns.is_empty() { + if let Some(table_cols) = metadata.get_table_columns() { + Rc::new(perspective_client::config::ViewConfig { + columns: table_cols.iter().map(|c| Some(c.clone())).collect(), + ..(**prop_config).clone() + }) + } else { + prop_config.clone() + } + } else { + prop_config.clone() + }; let is_aggregated = config.is_aggregated(); - let columns_iter = ctx.props().column_selector_iter_set(&config); - let onselect = ctx.link().callback(|()| ViewCreated); + let columns_iter = ColumnsIteratorSet::new(&config, metadata, renderer, dragdrop); + let onselect = ctx.link().callback(|()| Redraw); let ondragenter = ctx.link().callback(HoverActiveIndex); let ondragover = Callback::from(|_event: DragEvent| _event.prevent_default()); let ondrop = Callback::from({ @@ -245,7 +274,7 @@ impl Component for ColumnSelector { }); let mut active_classes = classes!(); - if ctx.props().dragdrop.get_drag_column().is_some() { + if ctx.props().drag_column.is_some() { active_classes.push("dragdrop-highlight"); }; @@ -258,8 +287,7 @@ impl Component for ColumnSelector { + config.split_by.len() + config.filter.len() + config.sort.len()) as f64, - session - .metadata() + metadata .get_features() .map(|x| { let mut y = 0.0; @@ -288,7 +316,10 @@ impl Component for ColumnSelector { }; - let mut named_count = self.named_row_count; + let mut named_count = ctx.props().named_column_count; let mut active_columns: Vec<_> = columns_iter .active() .enumerate() - .map(|(idx, name)| { + .map(|(idx, name): (usize, ActiveColumnState)| { let ondragenter = ondragenter.reform(move |_| Some(idx)); let size_hint = if named_count > 0 { 50.0 } else { 28.0 }; named_count = named_count.saturating_sub(1); @@ -315,6 +346,34 @@ impl Component for ColumnSelector { Some(ColumnLocator::Table(x)) | Some(ColumnLocator::Expression(x)) if x == &key ); + // Compute metadata-derived props here so that changes to + // session metadata propagate via prop diffing. + // For DragOver placeholders, resolve the type from the + // dragged column (since `get_name()` returns `None`). + let col_type = name + .get_name() + .and_then(|n| metadata.get_column_table_type(n)) + .or_else(|| { + if matches!(name.state, ActiveColumnStateData::DragOver) { + dragdrop + .get_drag_column() + .and_then(|c| metadata.get_column_table_type(&c)) + } else { + None + } + }); + + let is_expression = name + .get_name() + .map(|n| metadata.is_column_expression(n)) + .unwrap_or(false); + + let can_render_styles = name + .get_name() + .and_then(|n| can_render_column_styles(renderer, &config, metadata, n).ok()) + .unwrap_or(false); + + let show_edit_btn = is_expression || can_render_styles; let on_open_expr_panel = &ctx.props().on_open_expr_panel; html_nested! { @@ -323,6 +382,11 @@ impl Component for ColumnSelector { {idx} {is_aggregated} {is_editing} + {is_expression} + {show_edit_btn} + {col_type} + view_config={config.clone()} + metadata={metadata.clone()} {name} {on_open_expr_panel} {ondragenter} @@ -344,6 +408,7 @@ impl Component for ColumnSelector { .map(|(idx, vc)| { let selected_column = ctx.props().selected_column.as_ref(); let is_editing = matches!(selected_column, Some(ColumnLocator::Expression(x)) if x.as_str() == vc.name); + let is_expression = metadata.is_column_expression(vc.name); html_nested! { , + + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + + /// View config snapshot — threaded from parent as a value prop. + pub view_config: Rc, + /// State pub session: Session, pub dragdrop: DragDrop, @@ -71,8 +91,16 @@ pub struct ActiveColumnProps { } impl PartialEq for ActiveColumnProps { - fn eq(&self, _rhs: &Self) -> bool { - false + fn eq(&self, rhs: &Self) -> bool { + self.idx == rhs.idx + && self.name == rhs.name + && self.is_aggregated == rhs.is_aggregated + && self.is_editing == rhs.is_editing + && self.is_expression == rhs.is_expression + && self.show_edit_btn == rhs.show_edit_btn + && self.col_type == rhs.col_type + && self.metadata == rhs.metadata + && self.view_config == rhs.view_config } } @@ -121,7 +149,7 @@ impl Component for ActiveColumn { is_render }, New(InPlaceColumn::Column(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); if ctx.props().idx >= view_config.columns.len() { view_config.columns.push(Some(col)); } else { @@ -133,15 +161,21 @@ impl Component for ActiveColumn { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } true }, New(InPlaceColumn::Expression(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); if ctx.props().idx >= view_config.columns.len() { view_config.columns.push(Some(col.name.as_ref().to_owned())); } else { @@ -155,10 +189,16 @@ impl Component for ActiveColumn { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } true }, @@ -234,7 +274,7 @@ impl Component for ActiveColumn { }) .collect(); - let col_type = ctx.props().get_table_type(&ctx.props().name); + let col_type = ctx.props().col_type; match (name, col_type) { ((label, ColumnState::Empty), _) => { classes.push("empty-named"); @@ -242,8 +282,7 @@ impl Component for ActiveColumn { let on_select = ctx.link().callback(ActiveColumnMsg::New); let exclude = ctx .props() - .session - .get_view_config() + .view_config .columns .iter() .flatten() @@ -292,11 +331,12 @@ impl Component for ActiveColumn { })) }; - let ondragend = &ctx.props().ondragend.reform(|_| {}); + let ondragend = &ctx.props().ondragend.reform(|_| tracing::error!("dragend")); let ondragstart = ctx.link().callback({ let event_name = name.to_owned(); let dragdrop = ctx.props().dragdrop.clone(); move |event: DragEvent| { + tracing::error!("dragstart"); dragdrop.set_drag_image(&event).unwrap(); dragdrop.notify_drag_start( event_name.to_string(), @@ -312,17 +352,12 @@ impl Component for ActiveColumn { .link() .callback(|event: MouseEvent| MouseEnter(event.which() == 0)); - let is_expression = ctx.props().session.metadata().is_column_expression(&name); + let is_expression = ctx.props().is_expression; + let show_edit_btn = ctx.props().show_edit_btn; let mut class = ctx.props().renderer.metadata().mode.css(); if is_required { class.push("required"); }; - - let can_render_styles = ctx - .props() - .can_render_column_styles(&name) - .unwrap_or_default(); - let show_edit_btn = is_expression || can_render_styles; html! {

@@ -390,27 +427,6 @@ impl Component for ActiveColumn { } impl ActiveColumnProps { - fn get_name(&self, defn: &ActiveColumnState) -> Option { - match &defn.state { - ActiveColumnStateData::DragOver => Some(self.dragdrop.get_drag_column().unwrap()), - ActiveColumnStateData::Column(name) => Some(name.to_owned()), - ActiveColumnStateData::Required => None, - ActiveColumnStateData::Invalid => None, - } - } - - fn get_table_type(&self, defn: &ActiveColumnState) -> Option { - self.get_name(defn) - .as_ref() - .and_then(|x| self.session.metadata().get_column_table_type(x)) - } - - fn _get_view_type(&self, defn: &ActiveColumnState) -> Option { - self.get_name(defn) - .as_ref() - .and_then(|x| self.session.metadata().get_column_view_type(x)) - } - /// Remove an active column from `columns`, or alternatively make this /// column the only column in `columns` if the shift key is set (via the /// `shift` flag). @@ -420,7 +436,7 @@ impl ActiveColumnProps { /// with respect to `columns`. /// - `shift` whether to toggle or select this column. fn deactivate_column(&self, name: String, shift: bool) { - let mut columns = self.session.get_view_config().columns.clone(); + let mut columns = self.view_config.columns.clone(); let max_cols = self .renderer .metadata() @@ -457,7 +473,7 @@ impl ActiveColumnProps { } fn get_aggregate(&self, name: &str) -> Option { - self.session.get_view_config().aggregates.get(name).cloned() + self.view_config.aggregates.get(name).cloned() } fn apply_columns(&self, columns: Vec>) { @@ -466,8 +482,13 @@ impl ActiveColumnProps { ..ViewConfigUpdate::default() }; - self.update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + if self.session.update_view_config(config).is_ok() { + let session = self.session.clone(); + let renderer = self.renderer.clone(); + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } } } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs index c53f145759..2aceef4f18 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs @@ -14,18 +14,16 @@ use std::collections::HashSet; use std::rc::Rc; use perspective_client::config::*; -use perspective_client::utils::PerspectiveResultExt; use perspective_js::utils::ApiFuture; use yew::prelude::*; use crate::components::containers::select::*; use crate::components::style::LocalStyle; -use crate::model::*; +use crate::css; use crate::renderer::*; use crate::session::*; -use crate::{PerspectiveProperties, css}; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Properties)] pub struct AggregateSelectorProps { /// The name of this aggregate. pub column: String, @@ -33,6 +31,12 @@ pub struct AggregateSelectorProps { /// Which aggregate is currently selected. pub aggregate: Option, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + + /// View config snapshot — threaded from parent as a value prop. + pub view_config: Rc, + // State pub renderer: Renderer, pub session: Session, @@ -40,7 +44,10 @@ pub struct AggregateSelectorProps { impl PartialEq for AggregateSelectorProps { fn eq(&self, rhs: &Self) -> bool { - self.column == rhs.column && self.aggregate == rhs.aggregate + self.column == rhs.column + && self.aggregate == rhs.aggregate + && self.metadata == rhs.metadata + && self.view_config == rhs.view_config } } @@ -89,11 +96,10 @@ impl Component for AggregateSelector { .clone() .or_else(|| { ctx.props() - .session - .metadata() + .metadata .get_column_table_type(&ctx.props().column) .and_then(|x| { - ctx.props().session.metadata().get_features().and_then(|y| { + ctx.props().metadata.get_features().and_then(|y| { y.aggregates.get(&(x as u32)).and_then(|z| { z.aggregates .first() @@ -130,24 +136,27 @@ impl Component for AggregateSelector { impl AggregateSelector { pub fn set_aggregate(&mut self, ctx: &Context, aggregate: Aggregate) { self.aggregate = Some(aggregate.clone()); - let mut aggregates = ctx.props().session.get_view_config().aggregates.clone(); + let mut aggregates = ctx.props().view_config.aggregates.clone(); aggregates.insert(ctx.props().column.clone(), aggregate); let config = ViewConfigUpdate { aggregates: Some(aggregates), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } } pub fn get_dropdown_aggregates(&self, ctx: &Context) -> Vec> { let aggregates = ctx .props() - .session - .metadata() + .metadata .get_column_aggregates(&ctx.props().column) .map(|x| x.collect::>()) .unwrap_or_default(); diff --git a/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs index 654fcc7211..8c291a0a5d 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs @@ -13,8 +13,7 @@ use std::collections::HashSet; use std::rc::Rc; -use perspective_client::config::*; -use perspective_client::utils::PerspectiveResultExt; +use perspective_client::config::{ViewConfig, *}; use perspective_js::utils::ApiFuture; use yew::prelude::*; @@ -25,21 +24,31 @@ use super::sort_column::*; use crate::components::containers::dragdrop_list::*; use crate::components::containers::select::{Select, SelectItem}; use crate::components::style::LocalStyle; +use crate::css; use crate::custom_elements::{ColumnDropDownElement, FilterDropDownElement}; use crate::dragdrop::*; -use crate::model::*; use crate::renderer::*; +use crate::session::drag_drop_update::*; use crate::session::*; use crate::utils::*; -use crate::{PerspectiveProperties, css}; -#[derive(Clone, Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct ConfigSelectorProps { pub onselect: Callback<()>, #[prop_or_default] pub ondragenter: Callback<()>, + /// Current view config threaded as a value prop so that config changes + /// (group_by, sort, filter, etc.) trigger re-renders via normal prop + /// diffing rather than a PubSub `view_created` subscription. + pub view_config: Rc, + /// Column currently being dragged — threaded to show `dragdrop-highlight` + /// without subscribing to `dragstart_received`/`dragend_received`. + pub drag_column: Option, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + // State pub session: Session, pub renderer: Renderer, @@ -47,22 +56,21 @@ pub struct ConfigSelectorProps { } impl PartialEq for ConfigSelectorProps { - fn eq(&self, _other: &Self) -> bool { - false + fn eq(&self, other: &Self) -> bool { + self.view_config == other.view_config + && self.drag_column == other.drag_column + && self.metadata == other.metadata } } #[derive(Debug)] pub enum ConfigSelectorMsg { - DragStart, - DragEnd, DragOver(usize, DragTarget), DragLeave(DragTarget), Drop(String, DragTarget, DragEffect, usize), Close(usize, DragTarget), SetFilterValue(usize, String), TransposePivots, - ViewCreated, New(DragTarget, InPlaceColumn), UpdateGroupRollupMode(GroupRollupMode), } @@ -71,7 +79,7 @@ pub enum ConfigSelectorMsg { pub struct ConfigSelector { filter_dropdown: FilterDropDownElement, column_dropdown: ColumnDropDownElement, - _subscriptions: [Rc; 4], + _subscriptions: [Rc; 1], } impl Component for ConfigSelector { @@ -79,12 +87,6 @@ impl Component for ConfigSelector { type Properties = ConfigSelectorProps; fn create(ctx: &Context) -> Self { - let cb = ctx.link().callback(|_| ConfigSelectorMsg::DragStart); - let drag_sub = Rc::new(ctx.props().dragdrop.dragstart_received.add_listener(cb)); - - let cb = ctx.link().callback(|_| ConfigSelectorMsg::DragEnd); - let dragend_sub = Rc::new(ctx.props().dragdrop.dragend_received.add_listener(cb)); - let cb = ctx .link() .callback(|x: (String, DragTarget, DragEffect, usize)| { @@ -92,12 +94,9 @@ impl Component for ConfigSelector { }); let drop_sub = Rc::new(ctx.props().dragdrop.drop_received.add_listener(cb)); - let cb = ctx.link().callback(|_| ConfigSelectorMsg::ViewCreated); - let view_sub = Rc::new(ctx.props().session.view_created.add_listener(cb)); - let filter_dropdown = FilterDropDownElement::new(ctx.props().session.clone()); let column_dropdown = ColumnDropDownElement::new(ctx.props().session.clone()); - let _subscriptions = [drop_sub, view_sub, drag_sub, dragend_sub]; + let _subscriptions = [drop_sub]; Self { filter_dropdown, column_dropdown, @@ -107,8 +106,6 @@ impl Component for ConfigSelector { fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - ConfigSelectorMsg::DragStart | ConfigSelectorMsg::ViewCreated => true, - ConfigSelectorMsg::DragEnd => true, ConfigSelectorMsg::DragOver(index, action) => { let should_render = ctx.props().dragdrop.notify_drag_enter(action, index); if should_render { @@ -121,7 +118,7 @@ impl Component for ConfigSelector { true }, ConfigSelectorMsg::Close(index, DragTarget::Sort) => { - let mut sort = ctx.props().session.get_view_config().sort.clone(); + let mut sort = ctx.props().view_config.sort.clone(); sort.remove(index); let sort = Some(sort); let config = ViewConfigUpdate { @@ -129,10 +126,16 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false @@ -143,22 +146,26 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } false }, ConfigSelectorMsg::Close(index, DragTarget::GroupBy) => { - if ctx.props().session.get_view_config().group_rollup_mode == GroupRollupMode::Total - { + if ctx.props().view_config.group_rollup_mode == GroupRollupMode::Total { let requirements = ctx.props().renderer.metadata(); let rollup_features = ctx .props() - .session - .metadata() + .metadata .get_features() .map(|x| x.get_group_rollup_modes()) .unwrap(); @@ -171,51 +178,69 @@ impl Component for ConfigSelector { )); false } else { - let mut group_by = ctx.props().session.get_view_config().group_by.clone(); + let mut group_by = ctx.props().view_config.group_by.clone(); group_by.remove(index); let config = ViewConfigUpdate { group_by: Some(group_by), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false } }, ConfigSelectorMsg::Close(index, DragTarget::SplitBy) => { - let mut split_by = ctx.props().session.get_view_config().split_by.clone(); + let mut split_by = ctx.props().view_config.split_by.clone(); split_by.remove(index); let config = ViewConfigUpdate { split_by: Some(split_by), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::Close(index, DragTarget::Filter) => { self.filter_dropdown.hide().unwrap(); - let mut filter = ctx.props().session.get_view_config().filter.clone(); + let mut filter = ctx.props().view_config.filter.clone(); filter.remove(index); let config = ViewConfigUpdate { filter: Some(filter), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false @@ -224,18 +249,31 @@ impl Component for ConfigSelector { ConfigSelectorMsg::Drop(column, action, effect, index) if action != DragTarget::Active => { - let update = ctx.props().session.create_drag_drop_update( + let col_type = ctx + .props() + .metadata + .get_column_table_type(column.as_str()) + .unwrap(); + let update = ctx.props().view_config.create_drag_drop_update( column, + col_type, index, action, effect, &ctx.props().renderer.metadata(), + ctx.props().metadata.get_features().unwrap(), ); - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false @@ -247,7 +285,7 @@ impl Component for ConfigSelector { }, ConfigSelectorMsg::Drop(..) => false, ConfigSelectorMsg::TransposePivots => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); std::mem::swap(&mut view_config.group_by, &mut view_config.split_by); let update = ViewConfigUpdate { @@ -256,16 +294,22 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::SetFilterValue(index, input) => { - let mut filter = ctx.props().session.get_view_config().filter.clone(); + let mut filter = ctx.props().view_config.filter.clone(); // TODO Can't special case these - need to make this part of the // Features API. @@ -295,47 +339,65 @@ impl Component for ConfigSelector { } }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } false }, ConfigSelectorMsg::New(DragTarget::GroupBy, InPlaceColumn::Column(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config.group_by.push(col); let update = ViewConfigUpdate { group_by: Some(view_config.group_by), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::SplitBy, InPlaceColumn::Column(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config.split_by.push(col); let update = ViewConfigUpdate { split_by: Some(view_config.split_by), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::Filter, InPlaceColumn::Column(column)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); let op = ctx.props().default_op(column.as_str()).unwrap_or_default(); view_config.filter.push(Filter::new( &column, @@ -348,32 +410,44 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::Sort, InPlaceColumn::Column(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config.sort.push(Sort(col, SortDir::Asc)); let update = ViewConfigUpdate { sort: Some(view_config.sort), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::GroupBy, InPlaceColumn::Expression(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config.group_by.push(col.name.as_ref().to_owned()); view_config.expressions.insert(&col); let update = ViewConfigUpdate { @@ -382,16 +456,22 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::SplitBy, InPlaceColumn::Expression(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config.split_by.push(col.name.as_ref().to_owned()); view_config.expressions.insert(&col); let update = ViewConfigUpdate { @@ -400,16 +480,22 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::Filter, InPlaceColumn::Expression(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); let column = col.name.as_ref(); view_config.filter.push(Filter::new( column, @@ -426,16 +512,22 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::Sort, InPlaceColumn::Expression(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config .sort .push(Sort(col.name.as_ref().to_owned(), SortDir::Asc)); @@ -446,10 +538,16 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false @@ -471,12 +569,12 @@ impl Component for ConfigSelector { session, .. } = ctx.props(); - let config = session.get_view_config(); + let config = &ctx.props().view_config; let transpose = ctx.link().callback(|_| ConfigSelectorMsg::TransposePivots); let column_dropdown = self.column_dropdown.clone(); let mut class = classes!(); - if dragdrop.get_drag_column().is_some() { + if ctx.props().drag_column.is_some() { class.push("dragdrop-highlight"); } @@ -489,17 +587,14 @@ impl Component for ConfigSelector { move |_event| dragdrop.notify_drag_end() }); - let metadata = session.metadata(); + let metadata = &ctx.props().metadata; let features = metadata.get_features().unwrap(); let requirements = renderer.metadata(); let on_group_rollup_mode = ctx .link() .callback(ConfigSelectorMsg::UpdateGroupRollupMode); - let rollup_features = ctx - .props() - .session - .metadata() + let rollup_features = metadata .get_features() .map(|x| x.get_group_rollup_modes()) .unwrap(); @@ -548,6 +643,7 @@ impl Component for ConfigSelector { @@ -580,6 +676,7 @@ impl Component for ConfigSelector { @@ -604,6 +701,8 @@ impl Component for ConfigSelector { @@ -634,6 +733,8 @@ impl Component for ConfigSelector { filter_dropdown={ &self.filter_dropdown } filter={ filter.clone() } on_keydown={ filter_keydown } + view_config={config.clone()} + metadata={metadata.clone()} {dragdrop} {renderer} {session}> @@ -649,9 +750,8 @@ impl Component for ConfigSelector { impl ConfigSelectorProps { fn default_op(&self, column: &str) -> Option { - let metadata = self.session.metadata(); - let features = metadata.get_features()?; - let col_type = metadata.get_column_table_type(column)?; + let features = self.metadata.get_features()?; + let col_type = self.metadata.get_column_table_type(column)?; let first = features.default_op(col_type)?; Some(first.to_string()) } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs index a0808d255c..70c9cc2ae6 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs @@ -15,7 +15,6 @@ use std::rc::Rc; use chrono::{Datelike, NaiveDate, TimeZone, Utc}; use perspective_client::config::*; -use perspective_client::utils::PerspectiveResultExt; use perspective_js::utils::ApiFuture; use wasm_bindgen::JsCast; use web_sys::*; @@ -27,19 +26,23 @@ use crate::components::style::LocalStyle; use crate::components::type_icon::TypeIcon; use crate::custom_elements::*; use crate::dragdrop::*; -use crate::model::*; use crate::renderer::*; use crate::session::*; use crate::utils::*; -use crate::*; +use crate::{css, maybe}; -#[derive(Clone, Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct FilterColumnProps { pub filter: Filter, pub idx: usize, pub filter_dropdown: FilterDropDownElement, pub on_keydown: Callback, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + /// Current view config threaded as a value prop. + pub view_config: Rc, + // State pub session: Session, pub renderer: Renderer, @@ -48,7 +51,11 @@ pub struct FilterColumnProps { impl PartialEq for FilterColumnProps { fn eq(&self, rhs: &Self) -> bool { - self.idx == rhs.idx && self.filter == rhs.filter && self.on_keydown == rhs.on_keydown + self.idx == rhs.idx + && self.filter == rhs.filter + && self.on_keydown == rhs.on_keydown + && self.metadata == rhs.metadata + && self.view_config == rhs.view_config } } @@ -95,7 +102,7 @@ impl Component for FilterColumn { this.filter_ops = Rc::new( maybe! { - Some(get_filter_ops(ctx.props().session(), col_type?)? + Some(get_filter_ops(&ctx.props().metadata, col_type?)? .into_iter() .map(SelectItem::Option) .collect::>()) @@ -183,7 +190,7 @@ impl Component for FilterColumn { changed = true; self.filter_ops = Rc::new( maybe! { - Some(get_filter_ops(&ctx.props().session, col_type?)? + Some(get_filter_ops(&ctx.props().metadata, col_type?)? .into_iter() .map(SelectItem::Option) .collect::>()) @@ -204,11 +211,7 @@ impl Component for FilterColumn { let idx = ctx.props().idx; let filter = ctx.props().filter.clone(); let column = filter.column().to_owned(); - let col_type = ctx - .props() - .session - .metadata() - .get_column_table_type(&column); + let col_type = ctx.props().metadata.get_column_table_type(&column); let select = ctx.link().callback(FilterColumnMsg::FilterOpSelect); let noderef = &self.input_ref; let input = ctx.link().callback({ @@ -377,8 +380,7 @@ impl Component for FilterColumn { } /// Get the allowed `FilterOp`s for this filter. -fn get_filter_ops(session: &Session, col_type: ColumnType) -> Option> { - let metadata = session.metadata(); +fn get_filter_ops(metadata: &SessionMetadata, col_type: ColumnType) -> Option> { let features = metadata.get_features()?; features .filter_ops @@ -404,9 +406,7 @@ impl FilterColumnProps { /// Get this filter's type, e.g. the type of the column. fn get_filter_type(&self, filter: &Filter) -> Option { - self.session - .metadata() - .get_column_table_type(filter.column()) + self.metadata.get_column_table_type(filter.column()) } // Get the string value, suitable for the `value` field of a `FilterColumns`'s @@ -442,7 +442,7 @@ impl FilterColumnProps { /// # Arguments /// - `op` The new `FilterOp`. fn update_filter_op(&self, idx: usize, op: String) { - let mut filter = self.session.get_view_config().filter.clone(); + let mut filter = self.view_config.filter.clone(); let filter_column = &mut filter.get_mut(idx).expect("Filter on no column"); *filter_column.op_mut() = op; let update = ViewConfigUpdate { @@ -450,9 +450,14 @@ impl FilterColumnProps { ..ViewConfigUpdate::default() }; - self.update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + if self.session.update_view_config(update).is_ok() { + let session = self.session.clone(); + let renderer = self.renderer.clone(); + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } } /// Update the filter value from the string input read from the DOM. @@ -460,7 +465,7 @@ impl FilterColumnProps { /// # Arguments /// - `val` The new filter value. fn update_filter_input(&self, val: String) { - let mut filters = self.session.get_view_config().filter.clone(); + let mut filters = self.view_config.filter.clone(); let filter_column = &mut filters.get_mut(self.idx).expect("Filter on no column"); // TODO This belongs in the Features API. @@ -523,9 +528,14 @@ impl FilterColumnProps { ..ViewConfigUpdate::default() }; - self.update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + if self.session.update_view_config(update).is_ok() { + let session = self.session.clone(); + let renderer = self.renderer.clone(); + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } } } } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs index e8d8a9bf4c..38b2d689a2 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs @@ -10,9 +10,10 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +use std::rc::Rc; + use itertools::Itertools; use perspective_client::config::*; -use perspective_client::utils::PerspectiveResultExt; use perspective_js::utils::ApiFuture; use web_sys::*; use yew::prelude::*; @@ -21,14 +22,12 @@ use super::expr_edit_button::*; use crate::components::type_icon::TypeIcon; use crate::dragdrop::*; use crate::js::plugin::*; -use crate::model::*; use crate::presentation::ColumnLocator; use crate::renderer::*; use crate::session::*; use crate::utils::*; -use crate::*; -#[derive(Clone, Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct InactiveColumnProps { /// This column's index in its list. pub idx: usize, @@ -42,6 +41,18 @@ pub struct InactiveColumnProps { /// Is the expression/config panel open for this column? pub is_editing: bool, + /// Whether this column is an expression column. Computed by the parent + /// so that changes to session metadata trigger a re-render via prop diff. + #[prop_or_default] + pub is_expression: bool, + + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + + /// View config snapshot — threaded from parent so we avoid + /// `session.get_view_config()` calls. + pub view_config: Rc, + /// `dragend` event`. pub ondragend: Callback<()>, @@ -58,11 +69,14 @@ pub struct InactiveColumnProps { } impl PartialEq for InactiveColumnProps { - /// Equality for `InactiveColumnProps` determines when it should re-render, - /// which is only when it has changed. - /// TODO Aggregates - fn eq(&self, _rhs: &Self) -> bool { - false + fn eq(&self, rhs: &Self) -> bool { + self.idx == rhs.idx + && self.visible == rhs.visible + && self.name == rhs.name + && self.is_editing == rhs.is_editing + && self.is_expression == rhs.is_expression + && self.metadata == rhs.metadata + && self.view_config == rhs.view_config } } @@ -112,8 +126,7 @@ impl Component for InactiveColumn { fn view(&self, ctx: &Context) -> Html { let col_type = ctx .props() - .session - .metadata() + .metadata .get_column_table_type(&ctx.props().name) .unwrap_or(ColumnType::String); @@ -128,7 +141,7 @@ impl Component for InactiveColumn { move |event: DragEvent| { dragdrop.set_drag_image(&event).unwrap(); dragdrop.notify_drag_start(event_name.to_string(), DragEffect::Copy); - MouseLeave(false) + MouseLeave(true) } }); @@ -137,11 +150,7 @@ impl Component for InactiveColumn { .link() .callback(|event: MouseEvent| MouseEnter(event.which() == 0)); - let is_expression = ctx - .props() - .session - .metadata() - .is_column_expression(&ctx.props().name); + let is_expression = ctx.props().is_expression; let is_active_class = ctx.props().renderer.metadata().mode.css(); let mut class = classes!("column-selector-column"); @@ -191,7 +200,7 @@ impl InactiveColumnProps { /// with respect to `columns`. /// - `shift` whether to toggle or select this column. pub fn activate_column(&self, name: String, shift: bool) { - let mut columns = self.session.get_view_config().columns.clone(); + let mut columns = self.view_config.columns.clone(); let max_cols = self .renderer .metadata() @@ -229,8 +238,13 @@ impl InactiveColumnProps { ..ViewConfigUpdate::default() }; - self.update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + if self.session.update_view_config(config).is_ok() { + let session = self.session.clone(); + let renderer = self.renderer.clone(); + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } } } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/invalid_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/invalid_column.rs index dfcfe96ced..ea26c28d69 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/invalid_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/invalid_column.rs @@ -15,23 +15,12 @@ use yew::prelude::*; use crate::components::style::LocalStyle; use crate::css; -#[derive(Default)] -pub struct InvalidColumn {} - -impl Component for InvalidColumn { - type Message = (); - type Properties = (); - - fn create(_ctx: &Context) -> Self { - Self::default() - } - - fn view(&self, _ctx: &Context) -> Html { - html! { -
- -
-
- } +#[function_component(InvalidColumn)] +pub fn invalid_column() -> Html { + html! { +
+ +
+
} } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs index 71ecedab51..27ccc9bda9 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs @@ -10,6 +10,8 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +use std::rc::Rc; + use perspective_client::config::ColumnType; use web_sys::*; use yew::prelude::*; @@ -19,9 +21,8 @@ use crate::components::type_icon::TypeIcon; use crate::dragdrop::*; use crate::session::*; use crate::utils::*; -use crate::*; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Properties)] pub struct PivotColumnProps { /// Column name. pub column: String, @@ -32,6 +33,10 @@ pub struct PivotColumnProps { /// The drag starte of this column, if applicable. pub action: DragTarget, + /// Session metadata snapshot — threaded from `SessionProps`. + #[prop_or_default] + pub metadata: Option>, + // State #[prop_or_default] pub opt_session: Option, @@ -40,7 +45,9 @@ pub struct PivotColumnProps { impl PartialEq for PivotColumnProps { fn eq(&self, other: &Self) -> bool { - self.column == other.column && self.action == other.action + self.column == other.column + && self.action == other.action + && self.metadata == other.metadata } } @@ -80,9 +87,9 @@ impl Component for PivotColumn { let col_type = ctx.props().column_type.unwrap_or_else(|| { ctx.props() - .opt_session + .metadata .as_ref() - .and_then(|x| x.metadata().get_column_table_type(&ctx.props().column)) + .and_then(|x| x.get_column_table_type(&ctx.props().column)) .unwrap_or(ColumnType::Integer) }); diff --git a/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs index 02f7abea05..40ba61d4eb 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs @@ -10,8 +10,9 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +use std::rc::Rc; + use perspective_client::config::*; -use perspective_client::utils::PerspectiveResultExt; use perspective_js::utils::ApiFuture; use web_sys::*; use yew::prelude::*; @@ -19,17 +20,21 @@ use yew::prelude::*; use crate::components::containers::dragdrop_list::*; use crate::components::type_icon::TypeIcon; use crate::dragdrop::*; -use crate::model::*; use crate::renderer::*; use crate::session::*; use crate::utils::*; -use crate::*; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Properties)] pub struct SortColumnProps { pub sort: Sort, pub idx: usize, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + + /// Current view config — threaded as a value prop. + pub view_config: Rc, + // State pub session: Session, pub renderer: Renderer, @@ -38,7 +43,10 @@ pub struct SortColumnProps { impl PartialEq for SortColumnProps { fn eq(&self, other: &Self) -> bool { - self.sort == other.sort && self.idx == other.idx + self.sort == other.sort + && self.idx == other.idx + && self.metadata == other.metadata + && self.view_config == other.view_config } } @@ -69,8 +77,8 @@ impl Component for SortColumn { fn update(&mut self, ctx: &Context, msg: SortColumnMsg) -> bool { match msg { SortColumnMsg::SortDirClick(shift_key) => { - let is_split = ctx.props().session.get_view_config().split_by.is_empty(); - let mut sort = ctx.props().session.get_view_config().sort.clone(); + let is_split = ctx.props().view_config.split_by.is_empty(); + let mut sort = ctx.props().view_config.sort.clone(); let sort_column = &mut sort.get_mut(ctx.props().idx).expect("Sort on no column"); sort_column.1 = sort_column.1.cycle(!is_split, shift_key); let update = ViewConfigUpdate { @@ -78,10 +86,14 @@ impl Component for SortColumn { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } false }, @@ -110,8 +122,7 @@ impl Component for SortColumn { let col_type = ctx .props() - .session - .metadata() + .metadata .get_column_table_type(&ctx.props().sort.0.to_owned()) .unwrap_or(ColumnType::Integer); diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs index ab96691ee3..3c7b57a6a9 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs @@ -18,7 +18,7 @@ use std::rc::Rc; use derivative::Derivative; use itertools::Itertools; -use perspective_client::config::{ColumnType, Expression}; +use perspective_client::config::{ColumnType, Expression, ViewConfig}; use perspective_client::utils::PerspectiveResultExt; use yew::{Callback, Component, Html, Properties, html, props}; @@ -34,14 +34,16 @@ use crate::components::expression_editor::ExpressionEditorProps; use crate::components::style::LocalStyle; use crate::components::type_icon::TypeIconType; use crate::custom_events::CustomEvents; -use crate::model::*; use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation}; use crate::renderer::Renderer; -use crate::session::Session; -use crate::utils::*; +use crate::session::{Session, SessionMetadata}; +use crate::tasks::{ + EditExpression, HasCustomEvents, HasPresentation, HasRenderer, HasSession, + can_render_column_styles, locator_name_or_default, locator_view_type, +}; use crate::*; -#[derive(Clone, Derivative, Properties, PerspectiveProperties!)] +#[derive(Clone, Derivative, Properties)] #[derivative(Debug)] pub struct ColumnSettingsPanelProps { pub selected_column: ColumnLocator, @@ -50,6 +52,17 @@ pub struct ColumnSettingsPanelProps { pub width_override: Option, pub on_select_tab: Callback, + /// Active plugin name threaded as a value prop so that plugin changes + /// trigger re-initialization via `changed()` rather than a PubSub + /// `render_limits_changed` subscription. + pub plugin_name: Option, + + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + + /// View config snapshot — threaded from `SessionProps`. + pub view_config: Rc, + // State #[derivative(Debug = "ignore")] pub custom_events: CustomEvents, @@ -66,7 +79,35 @@ pub struct ColumnSettingsPanelProps { impl PartialEq for ColumnSettingsPanelProps { fn eq(&self, other: &Self) -> bool { - self.selected_column == other.selected_column && self.selected_tab == other.selected_tab + self.selected_column == other.selected_column + && self.selected_tab == other.selected_tab + && self.plugin_name == other.plugin_name + && self.metadata == other.metadata + && self.view_config == other.view_config + } +} + +impl HasCustomEvents for ColumnSettingsPanelProps { + fn custom_events(&self) -> &CustomEvents { + &self.custom_events + } +} + +impl HasPresentation for ColumnSettingsPanelProps { + fn presentation(&self) -> &Presentation { + &self.presentation + } +} + +impl HasRenderer for ColumnSettingsPanelProps { + fn renderer(&self) -> &Renderer { + &self.renderer + } +} + +impl HasSession for ColumnSettingsPanelProps { + fn session(&self) -> &Session { + &self.session } } @@ -80,7 +121,6 @@ pub enum ColumnSettingsPanelMsg { OnSaveAttributes(()), OnResetAttributes(()), OnDelete(()), - SessionUpdated(bool), } #[derive(Derivative)] @@ -102,9 +142,6 @@ pub struct ColumnSettingsPanel { save_count: u8, save_enabled: bool, tabs: Vec, - - #[derivative(Debug = "ignore")] - _session_sub: Option, } impl Component for ColumnSettingsPanel { @@ -112,18 +149,7 @@ impl Component for ColumnSettingsPanel { type Properties = ColumnSettingsPanelProps; fn create(ctx: &yew::prelude::Context) -> Self { - let session_cb = ctx - .link() - .callback(|(is_update, _)| ColumnSettingsPanelMsg::SessionUpdated(is_update)); - - let session_sub = ctx - .props() - .renderer - .render_limits_changed - .add_listener(session_cb); - let mut this = Self { - _session_sub: Some(session_sub), initial_expr_value: Rc::default(), expr_value: Rc::default(), expr_valid: false, @@ -233,14 +259,6 @@ impl Component for ColumnSettingsPanel { ctx.props().on_close.emit(()); true }, - ColumnSettingsPanelMsg::SessionUpdated(is_update) => { - if !is_update { - self.initialize(ctx); - true - } else { - false - } - }, } } @@ -264,6 +282,7 @@ impl Component for ColumnSettingsPanel { ColumnSettingsPanelMsg::SetHeaderValid(valid), ] }), + metadata: ctx.props().metadata.clone(), session: &ctx.props().session }); @@ -274,13 +293,27 @@ impl Component for ColumnSettingsPanel { alias: ctx.props().selected_column.name().cloned(), disabled: !ctx.props().selected_column.is_expr(), reset_count: self.reset_count, + metadata: ctx.props().metadata.clone(), session: &ctx.props().session }); let disable_delete = ctx .props() - .session - .is_locator_active(&ctx.props().selected_column); + .selected_column + .name() + .map(|name| { + let config = &ctx.props().view_config; + config.columns.iter().any(|maybe_col| { + maybe_col + .as_ref() + .map(|col| col == name) + .unwrap_or_default() + }) || config.group_by.iter().any(|col| col == name) + || config.split_by.iter().any(|col| col == name) + || config.filter.iter().any(|col| col.column() == name) + || config.sort.iter().any(|col| &col.0 == name) + }) + .unwrap_or_default(); let save_section = SaveSettingsProps { save_enabled: self.save_enabled, @@ -302,15 +335,17 @@ impl Component for ColumnSettingsPanel { save_section, }; - let style_tab = props!(StyleTabProps { + let style_tab = StyleTabProps { ty: self.maybe_ty, column_name: self.column_name.clone(), - group_by_depth: ctx.props().session.get_view_config().group_by.len() as u32, - custom_events: ctx.props().custom_events(), - presentation: ctx.props().presentation(), - renderer: ctx.props().renderer(), - session: ctx.props().session() - }); + group_by_depth: ctx.props().view_config.group_by.len() as u32, + view_config: ctx.props().view_config.clone(), + metadata: ctx.props().metadata.clone(), + custom_events: ctx.props().custom_events.clone(), + presentation: ctx.props().presentation.clone(), + renderer: ctx.props().renderer.clone(), + session: ctx.props().session.clone(), + }; let tab_children = self.tabs.iter().map(|tab| match tab { ColumnSettingsTab::Attributes => html! { }, @@ -356,15 +391,12 @@ impl ColumnSettingsPanel { } fn initialize(&mut self, ctx: &yew::prelude::Context) { - let column_name = ctx - .props() - .session - .locator_name_or_default(&ctx.props().selected_column); + let column_name = + locator_name_or_default(&ctx.props().metadata, &ctx.props().selected_column); let initial_expr_value = ctx .props() - .session - .metadata() + .metadata .get_expression_by_alias(&column_name) .unwrap_or_default(); @@ -372,19 +404,19 @@ impl ColumnSettingsPanel { let initial_header_value = (*initial_expr_value != column_name).then_some(column_name.clone()); - let maybe_ty = ctx - .props() - .session() - .locator_view_type(&ctx.props().selected_column); + let maybe_ty = locator_view_type(&ctx.props().metadata, &ctx.props().selected_column); let tabs = { let mut tabs = vec![]; let is_new_expr = ctx.props().selected_column.is_new_expr(); let show_styles = !is_new_expr - && ctx - .props() - .can_render_column_styles(&column_name) - .unwrap_or_default(); + && can_render_column_styles( + &ctx.props().renderer, + &ctx.props().view_config, + &ctx.props().metadata, + &column_name, + ) + .unwrap_or_default(); if !is_new_expr && show_styles { tabs.push(ColumnSettingsTab::Style); @@ -414,7 +446,6 @@ impl ColumnSettingsPanel { on_input, on_save, on_validate, - _session_sub: self._session_sub.take(), ..*self } } diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs index a64e8562ef..0322ef40f8 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs @@ -14,6 +14,8 @@ mod agg_depth_selector; mod stub; mod symbol; +use std::rc::Rc; + use itertools::Itertools; use perspective_client::config::ColumnType; use yew::{Html, Properties, function_component, html}; @@ -26,18 +28,26 @@ use crate::components::number_column_style::NumberColumnStyle; use crate::components::string_column_style::StringColumnStyle; use crate::components::style_controls::CustomNumberFormat; use crate::custom_events::CustomEvents; -use crate::model::*; use crate::presentation::Presentation; use crate::renderer::Renderer; use crate::session::Session; -use crate::*; +use crate::tasks::{ + HasCustomEvents, HasPresentation, HasRenderer, HasSession, SendPluginConfig, + get_column_style_control_options, +}; -#[derive(Clone, PartialEq, Properties, PerspectiveProperties!)] +#[derive(Clone, PartialEq, Properties)] pub struct StyleTabProps { pub ty: Option, pub column_name: String, pub group_by_depth: u32, + /// View config snapshot — threaded from parent. + pub view_config: Rc, + + /// Session metadata snapshot — threaded from parent. + pub metadata: Rc, + // State pub custom_events: CustomEvents, pub presentation: Presentation, @@ -45,123 +55,151 @@ pub struct StyleTabProps { pub session: Session, } +impl HasCustomEvents for StyleTabProps { + fn custom_events(&self) -> &CustomEvents { + &self.custom_events + } +} + +impl HasPresentation for StyleTabProps { + fn presentation(&self) -> &Presentation { + &self.presentation + } +} + +impl HasRenderer for StyleTabProps { + fn renderer(&self) -> &Renderer { + &self.renderer + } +} + +impl HasSession for StyleTabProps { + fn session(&self) -> &Session { + &self.session + } +} + #[function_component] pub fn StyleTab(props: &StyleTabProps) -> Html { - let config = props.presentation().get_columns_config(&props.column_name); + let config = props.presentation.get_columns_config(&props.column_name); let on_change = yew::use_callback( - (props.clone_state(), props.column_name.clone()), + (props.clone(), props.column_name.clone()), |config, (state, column_name)| { state.send_plugin_config(column_name, config); }, ); - let components = props - .get_column_style_control_options(&props.column_name) - .map(|opts| { - let mut components = vec![]; - if !props.session().get_view_config().group_by.is_empty() { - let aggregate_depth = config.as_ref().map(|x| x.aggregate_depth as f64); - components.push(("Aggregate Depth", html! { - - })); - } - - if let Some(default_config) = opts.datagrid_number_style { - let config = config - .as_ref() - .map(|config| config.datagrid_number_style.clone()); - - components.push(("Number Styles", html! { - - })); - } - if let Some(default_config) = opts.datagrid_string_style { - let config = config - .as_ref() - .map(|config| config.datagrid_string_style.clone()); - - components.push(("String Styles", html! { - - })); - } - - if let Some(default_config) = opts.datagrid_datetime_style { - let config = config - .as_ref() - .map(|config| config.datagrid_datetime_style.clone()); - - let enable_time_config = props.ty.unwrap() == ColumnType::Datetime; - components.push(("Datetime Styles", html! { - - })) - } - - if let Some(default_config) = opts.symbols { - let restored_config = config - .as_ref() - .map(|config| config.symbols.clone()) - .unwrap_or_default(); - - components.push(("Symbols", html! { - - })) - } - - if opts.number_string_format.unwrap_or_default() { - let restored_config = config - .as_ref() - .and_then(|config| config.number_format.clone()) - .unwrap_or_default(); - - components.push(("Number Formatting", html! { - - })); - } - - components - .into_iter() - .map(|(_title, component)| { - html! { -
- // { title } - { component } -
- } - }) - .collect_vec() - }) - .unwrap_or_else(|error| { - vec![html! { - - }] - }); + let components = get_column_style_control_options( + &props.renderer, + &props.view_config, + &props.metadata, + &props.column_name, + ) + .map(|opts| { + let mut components = vec![]; + if !props.view_config.group_by.is_empty() { + let aggregate_depth = config.as_ref().map(|x| x.aggregate_depth as f64); + components.push(("Aggregate Depth", html! { + + })); + } + + if let Some(default_config) = opts.datagrid_number_style { + let config = config + .as_ref() + .map(|config| config.datagrid_number_style.clone()); + + components.push(("Number Styles", html! { + + })); + } + if let Some(default_config) = opts.datagrid_string_style { + let config = config + .as_ref() + .map(|config| config.datagrid_string_style.clone()); + + components.push(("String Styles", html! { + + })); + } + + if let Some(default_config) = opts.datagrid_datetime_style { + let config = config + .as_ref() + .map(|config| config.datagrid_datetime_style.clone()); + + let enable_time_config = props.ty.unwrap() == ColumnType::Datetime; + components.push(("Datetime Styles", html! { + + })) + } + + if let Some(default_config) = opts.symbols { + let restored_config = config + .as_ref() + .map(|config| config.symbols.clone()) + .unwrap_or_default(); + + components.push(("Symbols", html! { + + })) + } + + if opts.number_string_format.unwrap_or_default() { + let restored_config = config + .as_ref() + .and_then(|config| config.number_format.clone()) + .unwrap_or_default(); + + components.push(("Number Formatting", html! { + + })); + } + + components + .into_iter() + .map(|(_title, component)| { + html! { +
+ // { title } + { component } +
+ } + }) + .collect_vec() + }) + .unwrap_or_else(|error| { + vec![html! { + + }] + }); html! {
diff --git a/rust/perspective-viewer/src/rust/components/copy_dropdown.rs b/rust/perspective-viewer/src/rust/components/copy_dropdown.rs index 10f1d4a9ce..f87cbfacbb 100644 --- a/rust/perspective-viewer/src/rust/components/copy_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/copy_dropdown.rs @@ -17,8 +17,8 @@ use yew::prelude::*; use super::containers::dropdown_menu::*; use super::modal::*; use super::style::StyleProvider; -use crate::model::*; use crate::renderer::*; +use crate::tasks::*; use crate::utils::*; type CopyDropDownMenuItem = DropDownMenuItem; @@ -39,9 +39,7 @@ impl ModalLink for CopyDropDownMenuProps { } } -pub struct CopyDropDownMenu { - _sub: Subscription, -} +pub struct CopyDropDownMenu {} impl Component for CopyDropDownMenu { type Message = (); @@ -49,13 +47,7 @@ impl Component for CopyDropDownMenu { fn create(ctx: &Context) -> Self { ctx.set_modal_link(); - let _sub = ctx - .props() - .renderer - .plugin_changed - .add_listener(ctx.link().callback(|_| ())); - - Self { _sub } + Self {} } fn update(&mut self, _ctx: &Context, _msg: Self::Message) -> bool { diff --git a/rust/perspective-viewer/src/rust/components/datetime_column_style.rs b/rust/perspective-viewer/src/rust/components/datetime_column_style.rs index 348b4a21ed..52494d5ba3 100644 --- a/rust/perspective-viewer/src/rust/components/datetime_column_style.rs +++ b/rust/perspective-viewer/src/rust/components/datetime_column_style.rs @@ -56,8 +56,10 @@ impl ModalLink for DatetimeColumnStyleProps { } impl PartialEq for DatetimeColumnStyleProps { - fn eq(&self, _other: &Self) -> bool { - false + fn eq(&self, other: &Self) -> bool { + self.enable_time_config == other.enable_time_config + && self.config == other.config + && self.default_config == other.default_config } } diff --git a/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs b/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs index f1407551e9..5bc823b589 100644 --- a/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs +++ b/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs @@ -40,8 +40,8 @@ impl ModalLink for DatetimeStyleCustomProps { } impl PartialEq for DatetimeStyleCustomProps { - fn eq(&self, _other: &Self) -> bool { - false + fn eq(&self, other: &Self) -> bool { + self.enable_time_config == other.enable_time_config && self.config == other.config } } diff --git a/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs b/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs index a2ebedacc3..a0c6ae944b 100644 --- a/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs +++ b/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs @@ -36,8 +36,8 @@ impl ModalLink for DatetimeStyleSimpleProps { } impl PartialEq for DatetimeStyleSimpleProps { - fn eq(&self, _other: &Self) -> bool { - false + fn eq(&self, other: &Self) -> bool { + self.enable_time_config == other.enable_time_config && self.config == other.config } } diff --git a/rust/perspective-viewer/src/rust/components/editable_header.rs b/rust/perspective-viewer/src/rust/components/editable_header.rs index 2280af03ca..7839cc82d9 100644 --- a/rust/perspective-viewer/src/rust/components/editable_header.rs +++ b/rust/perspective-viewer/src/rust/components/editable_header.rs @@ -18,10 +18,10 @@ use yew::{Callback, Component, Html, NodeRef, Properties, TargetCast, classes, h use super::type_icon::TypeIconType; use crate::components::type_icon::TypeIcon; -use crate::session::Session; -use crate::*; +use crate::maybe; +use crate::session::{Session, SessionMetadata}; -#[derive(Clone, PartialEq, Properties, PerspectiveProperties!)] +#[derive(Clone, PartialEq, Properties)] pub struct EditableHeaderProps { pub icon_type: Option, pub on_change: Callback<(Option, bool)>, @@ -32,6 +32,9 @@ pub struct EditableHeaderProps { #[prop_or_default] pub reset_count: u8, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + // State pub session: Session, } @@ -111,7 +114,7 @@ impl Component for EditableHeader { if !self.edited { return Some(true); } - let metadata = ctx.props().session.metadata(); + let metadata = &ctx.props().metadata; let expressions = metadata.get_expression_columns(); let found = metadata .get_table_columns()? diff --git a/rust/perspective-viewer/src/rust/components/export_dropdown.rs b/rust/perspective-viewer/src/rust/components/export_dropdown.rs index 4fe642a681..547c953190 100644 --- a/rust/perspective-viewer/src/rust/components/export_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/export_dropdown.rs @@ -18,8 +18,8 @@ use yew::prelude::*; use super::containers::dropdown_menu::*; use super::modal::{ModalLink, SetModalLink}; use super::style::StyleProvider; -use crate::model::*; use crate::renderer::*; +use crate::tasks::*; use crate::utils::*; use crate::*; @@ -49,7 +49,6 @@ pub enum ExportDropDownMenuMsg { #[derive(Default)] pub struct ExportDropDownMenu { title: String, - _sub: Option, input_ref: NodeRef, invalid: bool, } @@ -98,20 +97,12 @@ impl Component for ExportDropDownMenu { fn create(ctx: &Context) -> Self { ctx.set_modal_link(); - let _sub = Some( - ctx.props() - .renderer - .plugin_changed - .add_listener(ctx.link().callback(|_| ExportDropDownMenuMsg::TitleChange)), - ); - Self { title: ctx .props() .session .get_title() .unwrap_or_else(|| "untitled".to_owned()), - _sub, ..Default::default() } } diff --git a/rust/perspective-viewer/src/rust/components/expression_editor.rs b/rust/perspective-viewer/src/rust/components/expression_editor.rs index 66389a2ad9..aa37a04bbe 100644 --- a/rust/perspective-viewer/src/rust/components/expression_editor.rs +++ b/rust/perspective-viewer/src/rust/components/expression_editor.rs @@ -17,11 +17,10 @@ use yew::prelude::*; use super::form::code_editor::*; use super::style::LocalStyle; -use crate::model::*; -use crate::session::Session; +use crate::session::{Session, SessionMetadata}; use crate::*; -#[derive(Properties, PartialEq, PerspectiveProperties!, Clone)] +#[derive(Properties, PartialEq, Clone)] pub struct ExpressionEditorProps { pub on_save: Callback<()>, pub on_validate: Callback, @@ -32,6 +31,9 @@ pub struct ExpressionEditorProps { #[prop_or_default] pub reset_count: u8, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + // State pub session: Session, } @@ -55,7 +57,7 @@ impl Component for ExpressionEditor { fn create(ctx: &Context) -> Self { let oninput = ctx.link().callback(ExpressionEditorMsg::SetExpr); - let expr = initial_expr(&ctx.props().session, &ctx.props().alias); + let expr = initial_expr(&ctx.props().metadata, &ctx.props().alias); ctx.link() .send_message(Self::Message::SetExpr(expr.clone())); @@ -89,8 +91,8 @@ impl Component for ExpressionEditor { if self.error.is_none() { maybe!({ let alias = ctx.props().alias.as_ref()?; - let session = ctx.props().session(); - let old = session.metadata().get_expression_by_alias(alias)?; + let session = &ctx.props().session; + let old = ctx.props().metadata.get_expression_by_alias(alias)?; let is_edited = *self.expr != old; session .metadata_mut() @@ -140,7 +142,7 @@ impl Component for ExpressionEditor { { ctx.link() .send_message(ExpressionEditorMsg::SetExpr(initial_expr( - &ctx.props().session, + &ctx.props().metadata, &ctx.props().alias, ))); false @@ -150,10 +152,10 @@ impl Component for ExpressionEditor { } } -fn initial_expr(session: &Session, alias: &Option) -> Rc { +fn initial_expr(metadata: &SessionMetadata, alias: &Option) -> Rc { alias .as_ref() - .and_then(|alias| session.metadata().get_expression_by_alias(alias)) + .and_then(|alias| metadata.get_expression_by_alias(alias)) .unwrap_or_default() .into() } diff --git a/rust/perspective-viewer/src/rust/components/font_loader.rs b/rust/perspective-viewer/src/rust/components/font_loader.rs index 5fcab0878d..39a6a6bcd1 100644 --- a/rust/perspective-viewer/src/rust/components/font_loader.rs +++ b/rust/perspective-viewer/src/rust/components/font_loader.rs @@ -47,35 +47,18 @@ impl PartialEq for FontLoaderProps { } } -/// The `FontLoader` component ensures that fonts are loaded before they are -/// visible. -pub struct FontLoader {} - -impl Component for FontLoader { - type Message = (); - type Properties = FontLoaderProps; - - fn create(_ctx: &Context) -> Self { - Self {} - } - - fn update(&mut self, _ctx: &Context, _msg: ()) -> bool { - false - } +#[function_component(FontLoader)] +pub fn font_loader(props: &FontLoaderProps) -> Html { + if matches!(props.get_status(), FontLoaderStatus::Finished) { + html! {} + } else { + let inner = props + .get_fonts() + .iter() + .map(font_test_html) + .collect::(); - fn view(&self, ctx: &Context) -> yew::virtual_dom::VNode { - if matches!(ctx.props().get_status(), FontLoaderStatus::Finished) { - html! {} - } else { - let inner = ctx - .props() - .get_fonts() - .iter() - .map(font_test_html) - .collect::(); - - html! { <>{ inner } } - } + html! { <>{ inner } } } } diff --git a/rust/perspective-viewer/src/rust/components/form/debug.rs b/rust/perspective-viewer/src/rust/components/form/debug.rs index ae58c94bba..2e1c7bf7b2 100644 --- a/rust/perspective-viewer/src/rust/components/form/debug.rs +++ b/rust/perspective-viewer/src/rust/components/form/debug.rs @@ -20,21 +20,39 @@ use yew::prelude::*; use crate::components::containers::trap_door_panel::TrapDoorPanel; use crate::components::form::code_editor::CodeEditor; use crate::components::style::LocalStyle; +use crate::css; use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard}; -use crate::model::*; use crate::presentation::*; use crate::renderer::*; use crate::session::*; +use crate::tasks::*; use crate::utils::*; -use crate::{PerspectiveProperties, css}; -#[derive(PartialEq, Properties, PerspectiveProperties!)] +#[derive(Clone, PartialEq, Properties)] pub struct DebugPanelProps { pub presentation: Presentation, pub renderer: Renderer, pub session: Session, } +impl HasPresentation for DebugPanelProps { + fn presentation(&self) -> &Presentation { + &self.presentation + } +} + +impl HasRenderer for DebugPanelProps { + fn renderer(&self) -> &Renderer { + &self.renderer + } +} + +impl HasSession for DebugPanelProps { + fn session(&self) -> &Session { + &self.session + } +} + #[function_component(DebugPanel)] pub fn debug_panel(props: &DebugPanelProps) -> Html { let expr = use_state_eq(|| Rc::new("".to_string())); @@ -42,7 +60,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { let select_all = use_memo((), |()| PubSub::default()); let modified = use_state_eq(|| false); - use_effect_with((expr.setter(), props.clone_state()), { + use_effect_with((expr.setter(), props.clone()), { clone!(error, modified); move |(text, state)| { state.set_text(text.clone()); @@ -57,7 +75,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { )); let sub2 = state - .renderer() + .renderer .reset_changed .add_listener(state.reset_callback( text.clone(), @@ -66,7 +84,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { )); let sub3 = state - .session() + .session .view_config_changed .add_listener(state.reset_callback( text.clone(), @@ -90,7 +108,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { } }); - let onsave = use_callback((expr.clone(), error.clone(), props.clone_state()), { + let onsave = use_callback((expr.clone(), error.clone(), props.clone()), { clone!(modified); move |_, (text, error, props)| props.on_save(text, error, &modified) }); @@ -111,12 +129,12 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { }, ); - let onapply = use_callback((expr.clone(), error.clone(), props.clone_state()), { + let onapply = use_callback((expr.clone(), error.clone(), props.clone()), { clone!(modified); move |_, (text, error, props)| props.on_save(text, error, &modified) }); - let onreset = use_callback((expr.setter(), error.clone(), props.clone_state()), { + let onreset = use_callback((expr.setter(), error.clone(), props.clone()), { clone!(modified); move |_, (text, error, props)| { props.set_text(text.clone()); @@ -125,7 +143,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { } }); - let onpaste = use_callback((expr.clone(), error.clone(), props.clone_state()), { + let onpaste = use_callback((expr.clone(), error.clone(), props.clone()), { clone!(modified); move |_, (text, error, props)| { clone!(text, error, props, modified); @@ -181,12 +199,11 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { } } -impl DebugPanelPropsState { +impl DebugPanelProps { fn set_text(&self, setter: UseStateSetter>) { let props = self.clone(); ApiFuture::spawn(async move { - let task = props.get_viewer_config(); - let config = task.await?; + let config = props.get_viewer_config().await?; let json = JsValue::from_serde_ext(&config)?; let js_string = js_sys::JSON::stringify_with_replacer_and_space(&json, &JsValue::NULL, &2.into())?; diff --git a/rust/perspective-viewer/src/rust/components/main_panel.rs b/rust/perspective-viewer/src/rust/components/main_panel.rs index 5997edd3ea..54fc5be1d8 100644 --- a/rust/perspective-viewer/src/rust/components/main_panel.rs +++ b/rust/perspective-viewer/src/rust/components/main_panel.rs @@ -10,24 +10,50 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use futures::channel::oneshot::*; +use std::rc::Rc; + use perspective_js::utils::*; use wasm_bindgen::prelude::*; use yew::prelude::*; use super::render_warning::RenderWarning; use super::status_bar::StatusBar; -use crate::PerspectiveProperties; use crate::custom_events::CustomEvents; use crate::presentation::Presentation; +use crate::renderer::limits::RenderLimits; use crate::renderer::*; -use crate::session::*; +use crate::session::{Session, TableErrorState, ViewStats}; use crate::utils::*; -#[derive(Clone, Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct MainPanelProps { pub on_settings: Callback<()>, + /// Reset callback forwarded from the root component. Fired when the user + /// clicks the reset button; `bool` is `true` for a full reset (expressions + /// + column configs), `false` for config-only. + pub on_reset: Callback, + + /// Render-limit dimensions forwarded from the root's `RendererProps`. + /// `Some` when the active plugin is capping the rendered row/column count; + /// `None` when no limits are active (e.g. after a plugin change). + pub render_limits: Option, + + /// Value props from root's `SessionProps`, threaded to `StatusBar` / + /// `StatusIndicator`. + pub has_table: bool, + pub is_errored: bool, + pub stats: Option, + pub update_count: u32, + pub error: Option, + pub title: Option, + + /// Value props from root's `PresentationProps`, threaded to `StatusBar`. + pub is_settings_open: bool, + pub selected_theme: Option, + pub available_themes: Rc>, + pub is_workspace: bool, + /// State pub custom_events: CustomEvents, pub session: Session, @@ -36,28 +62,33 @@ pub struct MainPanelProps { } impl PartialEq for MainPanelProps { - fn eq(&self, _rhs: &Self) -> bool { - false + fn eq(&self, rhs: &Self) -> bool { + self.has_table == rhs.has_table + && self.is_errored == rhs.is_errored + && self.stats == rhs.stats + && self.update_count == rhs.update_count + && self.error == rhs.error + && self.title == rhs.title + && self.is_settings_open == rhs.is_settings_open + && self.selected_theme == rhs.selected_theme + && self.available_themes == rhs.available_themes + && self.is_workspace == rhs.is_workspace + && self.render_limits == rhs.render_limits } } impl MainPanelProps { fn is_title(&self) -> bool { - self.session.get_title().is_some() + self.title.is_some() } } #[derive(Debug)] pub enum MainPanelMsg { - Reset(bool, Option>), - RenderLimits(Option<(usize, usize, Option, Option)>), PointerEvent(web_sys::PointerEvent), - Error, } pub struct MainPanel { - _subscriptions: [Subscription; 2], - dimensions: Option<(usize, usize, Option, Option)>, main_panel_ref: NodeRef, } @@ -65,85 +96,14 @@ impl Component for MainPanel { type Message = MainPanelMsg; type Properties = MainPanelProps; - fn create(ctx: &Context) -> Self { - let session_sub = { - let callback = ctx.link().callback(move |(_, render_limits)| { - MainPanelMsg::RenderLimits(Some(render_limits)) - }); - - ctx.props() - .renderer - .render_limits_changed - .add_listener(callback) - }; - - let error_sub = ctx - .props() - .session - .table_errored - .add_listener(ctx.link().callback(|_| MainPanelMsg::Error)); - + fn create(_ctx: &Context) -> Self { Self { - _subscriptions: [session_sub, error_sub], - dimensions: None, main_panel_ref: NodeRef::default(), } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - MainPanelMsg::Error => true, - MainPanelMsg::Reset(all, sender) => { - ctx.props().presentation.set_open_column_settings(None); - - clone!( - ctx.props().renderer, - ctx.props().session, - ctx.props().presentation - ); - - ApiFuture::spawn(async move { - session - .reset(ResetOptions { - config: true, - expressions: all, - ..ResetOptions::default() - }) - .await?; - let columns_config = if all { - presentation.reset_columns_configs(); - None - } else { - Some(presentation.all_columns_configs()) - }; - - renderer.reset(columns_config.as_ref()).await?; - presentation.reset_available_themes(None).await; - if all { - presentation.reset_theme().await?; - } - - let result = renderer.draw(session.validate().await?.create_view()).await; - if let Some(sender) = sender { - sender.send(()).unwrap(); - } - - renderer.reset_changed.emit(()); - result - }); - - false - }, - - MainPanelMsg::RenderLimits(dimensions) => { - if self.dimensions != dimensions { - self.dimensions = dimensions; - true - } else { - false - } - }, - MainPanelMsg::PointerEvent(event) => { if event.target().map(JsValue::from) == self @@ -175,8 +135,7 @@ impl Component for MainPanel { .. } = ctx.props(); - let is_settings_open = - ctx.props().presentation.is_settings_open() && ctx.props().session.has_table(); + let is_settings_open = ctx.props().is_settings_open && ctx.props().has_table; let on_settings = (!is_settings_open).then(|| ctx.props().on_settings.clone()); @@ -189,14 +148,34 @@ impl Component for MainPanel { class.push("titled"); } - let on_reset = ctx.link().callback(|all| MainPanelMsg::Reset(all, None)); let pointerdown = ctx.link().callback(MainPanelMsg::PointerEvent); + let on_dismiss_warning = { + clone!(renderer, session); + Callback::from(move |_: ()| { + clone!(renderer, session); + ApiFuture::spawn(async move { + renderer.disable_active_plugin_render_warning(); + let view_task = session.get_view(); + renderer.update(view_task).await + }); + }) + }; html! {
- +
diff --git a/rust/perspective-viewer/src/rust/components/mod.rs b/rust/perspective-viewer/src/rust/components/mod.rs index a309e26168..dfea92f649 100644 --- a/rust/perspective-viewer/src/rust/components/mod.rs +++ b/rust/perspective-viewer/src/rust/components/mod.rs @@ -22,7 +22,6 @@ pub mod copy_dropdown; pub mod datetime_column_style; pub mod editable_header; pub mod empty_row; -pub mod error_message; pub mod export_dropdown; pub mod expression_editor; pub mod filter_dropdown; diff --git a/rust/perspective-viewer/src/rust/components/number_column_style.rs b/rust/perspective-viewer/src/rust/components/number_column_style.rs index a0fd1a9d58..6a2ba9b13c 100644 --- a/rust/perspective-viewer/src/rust/components/number_column_style.rs +++ b/rust/perspective-viewer/src/rust/components/number_column_style.rs @@ -65,8 +65,10 @@ impl ModalLink for NumberColumnStyleProps { } impl PartialEq for NumberColumnStyleProps { - fn eq(&self, _other: &Self) -> bool { - false + fn eq(&self, other: &Self) -> bool { + self.config == other.config + && self.default_config == other.default_config + && self.column_name == other.column_name } } diff --git a/rust/perspective-viewer/src/rust/components/plugin_selector.rs b/rust/perspective-viewer/src/rust/components/plugin_selector.rs index cb44d0d2b5..e22bfd56c7 100644 --- a/rust/perspective-viewer/src/rust/components/plugin_selector.rs +++ b/rust/perspective-viewer/src/rust/components/plugin_selector.rs @@ -10,106 +10,53 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use perspective_client::config::ViewConfigUpdate; -use perspective_js::utils::ApiFuture; +use std::rc::Rc; + use yew::prelude::*; -use super::containers::select::*; use super::style::LocalStyle; -use crate::config::*; -use crate::js::*; -use crate::model::*; -use crate::presentation::Presentation; -use crate::renderer::*; -use crate::session::*; -use crate::utils::*; -use crate::{css, *}; - -#[derive(Properties, PartialEq, PerspectiveProperties!)] +use crate::css; + +/// Pure value props — no engine handles, no PubSub subscriptions. +/// The parent passes updated values whenever the renderer state changes. +#[derive(Properties, PartialEq)] pub struct PluginSelectorProps { - pub presentation: Presentation, - pub renderer: Renderer, - pub session: Session, + /// Name of the currently active plugin. + pub plugin_name: Option, + + /// Flat list of all registered plugin names (all categories merged). + pub available_plugins: Rc>, + + /// Called when the user selects a different plugin. + pub on_select_plugin: Callback, } #[derive(Debug)] pub enum PluginSelectorMsg { ComponentSelectPlugin(String), - RendererSelectPlugin(String), OpenMenu, } use PluginSelectorMsg::*; pub struct PluginSelector { - options: Vec>, is_open: bool, - _plugin_sub: Subscription, } impl Component for PluginSelector { type Message = PluginSelectorMsg; type Properties = PluginSelectorProps; - fn create(ctx: &Context) -> Self { - let PluginSelectorProps { renderer, .. } = ctx.props(); - let options = generate_plugin_optgroups(renderer); - let _plugin_sub = renderer.plugin_changed.add_listener({ - let link = ctx.link().clone(); - move |plugin: JsPerspectiveViewerPlugin| { - let name = plugin.name(); - link.send_message(PluginSelectorMsg::RendererSelectPlugin(name)) - } - }); - - Self { - options, - is_open: false, - _plugin_sub, - } + fn create(_ctx: &Context) -> Self { + Self { is_open: false } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - let PluginSelectorProps { - presentation, - renderer, - session, - .. - } = ctx.props(); match msg { - RendererSelectPlugin(_plugin_name) => true, ComponentSelectPlugin(plugin_name) => { - if !session.is_errored() { - let metadata = - renderer.get_next_plugin_metadata(&PluginUpdate::Update(plugin_name)); - - let prev_metadata = renderer.metadata(); - let requirements = metadata.as_ref().unwrap_or(&*prev_metadata); - let rollup_features = session - .metadata() - .get_features() - .map(|x| x.get_group_rollup_modes()) - .unwrap(); - - let group_rollups = requirements.get_group_rollups(&rollup_features); - let mut update = ViewConfigUpdate { - group_rollup_mode: group_rollups.first().cloned(), - ..ViewConfigUpdate::default() - }; - - session.set_update_column_defaults(&mut update, requirements); - - if let Ok(task) = ctx.props().update_and_render(update) { - ApiFuture::spawn(task); - } - - presentation.set_open_column_settings(None); - self.is_open = false; - false - } else { - self.is_open = false; - true - } + ctx.props().on_select_plugin.emit(plugin_name); + self.is_open = false; + false }, OpenMenu => { self.is_open = !self.is_open; @@ -124,22 +71,18 @@ impl Component for PluginSelector { fn view(&self, ctx: &Context) -> Html { let callback = ctx.link().callback(|_| OpenMenu); - let plugin_name = ctx.props().renderer.get_active_plugin().unwrap().name(); + let plugin_name = ctx.props().plugin_name.clone().unwrap_or_default(); let plugin_name2 = plugin_name.clone(); let class = if self.is_open { "open" } else { "" }; - let items = self.options.iter().map(|item| match item { - SelectItem::OptGroup(_cat, items) => html! { - items.iter().filter(|x| *x != &plugin_name2).map(|x| { - let callback = ctx.link().callback(ComponentSelectPlugin); - html! { - - } - }).collect::() - }, - SelectItem::Option(_item) => html! {}, - }); + let items = ctx + .props() + .available_plugins + .iter() + .filter(|x| x.as_str() != plugin_name2.as_str()) + .map(|x| { + let callback = ctx.link().callback(ComponentSelectPlugin); + html! { } + }); html! { <> @@ -156,19 +99,6 @@ impl Component for PluginSelector { } } -/// Generate the opt groups for the plugin selector by collecting by category -/// then sorting. -fn generate_plugin_optgroups(renderer: &Renderer) -> Vec> { - let mut options = renderer - .get_all_plugin_categories() - .into_iter() - .map(|(category, value)| SelectItem::OptGroup(category.into(), value)) - .collect::>(); - - options.sort_by_key(|x| x.name()); - options -} - #[derive(Properties, PartialEq)] struct PluginSelectProps { name: String, diff --git a/rust/perspective-viewer/src/rust/components/render_warning.rs b/rust/perspective-viewer/src/rust/components/render_warning.rs index 10cae11d61..4301cc68a6 100644 --- a/rust/perspective-viewer/src/rust/components/render_warning.rs +++ b/rust/perspective-viewer/src/rust/components/render_warning.rs @@ -13,144 +13,93 @@ use yew::prelude::*; use super::style::LocalStyle; -use crate::model::*; -use crate::renderer::*; -use crate::session::*; -use crate::*; +use crate::css; +use crate::renderer::limits::RenderLimits; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Properties, PartialEq)] pub struct RenderWarningProps { - // Current dimensions - pub dimensions: Option<(usize, usize, Option, Option)>, + pub dimensions: Option, - // State - pub renderer: Renderer, - pub session: Session, + /// Called when the user clicks "Render all points". The parent disables + /// the render warning on the active plugin and re-draws. + pub on_dismiss: Callback<()>, } -impl PartialEq for RenderWarningProps { - fn eq(&self, other: &Self) -> bool { - self.dimensions == other.dimensions - } -} - -pub enum RenderWarningMsg { - DismissWarning, -} - -pub struct RenderWarning { - col_warn: Option<(usize, usize)>, - row_warn: Option<(usize, usize)>, -} - -impl RenderWarning { - fn update_warnings(&mut self, ctx: &Context) { - if let Some((num_cols, num_rows, max_cols, max_rows)) = ctx.props().dimensions { - let count = num_cols * num_rows; - if max_cols.is_some_and(|x| x < num_cols) { - self.col_warn = Some((max_cols.unwrap(), num_cols)); - } else { - self.col_warn = None; - } - - if max_rows.is_some_and(|x| x < num_rows) { - self.row_warn = Some((num_cols * max_rows.unwrap(), count)); - } else { - self.row_warn = None; - } +#[function_component(RenderWarning)] +pub fn render_warning(props: &RenderWarningProps) -> Html { + let dimensions = props.dimensions; + let (col_warn, row_warn) = if let Some(limits) = dimensions { + let col_warn = if limits.max_cols.is_some_and(|x| x < limits.num_cols) { + Some((limits.max_cols.unwrap(), limits.num_cols)) } else { - self.col_warn = None; - self.row_warn = None; - } - } -} - -impl Component for RenderWarning { - type Message = RenderWarningMsg; - type Properties = RenderWarningProps; - - fn create(ctx: &Context) -> Self { - let mut elem = Self { - col_warn: None, - row_warn: None, + None }; - elem.update_warnings(ctx); - elem - } + let row_warn = if limits.max_rows.is_some_and(|x| x < limits.num_rows) { + Some(( + limits.num_cols * limits.max_rows.unwrap(), + limits.num_cols * limits.num_rows, + )) + } else { + None + }; - fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - match msg { - RenderWarningMsg::DismissWarning => { - let state = ctx.props().clone_state(); - ApiFuture::spawn(async move { - state.renderer().disable_active_plugin_render_warning(); - let view_task = state.session().get_view(); - state.renderer().update(view_task).await - }); + (col_warn, row_warn) + } else { + (None, None) + }; + + if col_warn.is_some() || row_warn.is_some() { + let warning = match (col_warn, row_warn) { + (Some((x, y)), Some((a, b))) => html! { + + { "Rendering" } + { render_pair(x, y) } + { "of columns and" } + { render_pair(a, b) } + { "of points." } + + }, + (Some((x, y)), None) => html! { + + { "Rendering" } + { render_pair(x, y) } + { "of columns." } + + }, + (None, Some((x, y))) => html! { + + { "Rendering" } + { render_pair(x, y) } + { "of points." } + }, + _ => html! {
}, }; - true - } - - fn changed(&mut self, ctx: &Context, _old: &Self::Properties) -> bool { - self.update_warnings(ctx); - true - } - fn view(&self, ctx: &Context) -> Html { - if self.col_warn.is_some() || self.row_warn.is_some() { - let warning = match (self.col_warn, self.row_warn) { - (Some((x, y)), Some((a, b))) => html! { - - { "Rendering" } - { render_pair(x, y) } - { "of columns and" } - { render_pair(a, b) } - { "of points." } + let on_dismiss = props.on_dismiss.clone(); + let onclick = Callback::from(move |_: MouseEvent| on_dismiss.emit(())); + html! { + <> + +
+ + + { warning } - }, - (Some((x, y)), None) => html! { - - { "Rendering" } - { render_pair(x, y) } - { "of columns." } - - }, - (None, Some((x, y))) => html! { - - { "Rendering" } - { render_pair(x, y) } - { "of points." } - - }, - _ => html! {
}, - }; - - let onclick = ctx.link().callback(|_| RenderWarningMsg::DismissWarning); - - html! { - <> - -
- - - { warning } + + + { "Render all points" } - - - { "Render all points" } - - -
- - } - } else { - html! {} + +
+ } + } else { + html! {} } } diff --git a/rust/perspective-viewer/src/rust/components/settings_panel.rs b/rust/perspective-viewer/src/rust/components/settings_panel.rs index 137325c522..4cc7418ed3 100644 --- a/rust/perspective-viewer/src/rust/components/settings_panel.rs +++ b/rust/perspective-viewer/src/rust/components/settings_panel.rs @@ -12,20 +12,23 @@ use std::rc::Rc; +use perspective_client::config::{ViewConfig, ViewConfigUpdate}; +use perspective_js::utils::ApiFuture; use yew::prelude::*; use super::column_selector::ColumnSelector; use super::plugin_selector::PluginSelector; -use crate::PerspectiveProperties; use crate::components::containers::sidebar_close_button::SidebarCloseButton; +use crate::config::PluginUpdate; use crate::dragdrop::*; -use crate::model::*; -use crate::presentation::{ColumnLocator, Presentation}; +use crate::presentation::{ColumnLocator, OpenColumnSettings, Presentation}; use crate::renderer::*; +use crate::session::column_defaults_update::*; use crate::session::*; +use crate::tasks::can_render_column_styles; use crate::utils::*; -#[derive(Clone, Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct SettingsPanelProps { pub on_close: Callback<()>, pub on_resize: Rc>, @@ -33,6 +36,22 @@ pub struct SettingsPanelProps { pub on_debug: Callback<()>, pub is_debug: bool, + /// Value props threaded from the root's `RendererProps` / `SessionProps`. + pub plugin_name: Option, + pub available_plugins: Rc>, + pub has_table: bool, + pub named_column_count: usize, + pub view_config: Rc, + /// Column currently being dragged (if any) — threaded to show drag + /// highlights without per-component `DragDrop` PubSub subscriptions. + pub drag_column: Option, + /// Cloned session metadata snapshot — threaded from `SessionProps` + /// so that metadata changes trigger re-renders via prop diffing. + pub metadata: Rc, + /// Snapshot of the column-settings sidebar state — threaded from + /// `PresentationProps` so that open/close triggers re-renders. + pub open_column_settings: OpenColumnSettings, + /// State pub dragdrop: DragDrop, pub session: Session, @@ -41,8 +60,16 @@ pub struct SettingsPanelProps { } impl PartialEq for SettingsPanelProps { - fn eq(&self, _rhs: &Self) -> bool { - false + fn eq(&self, rhs: &Self) -> bool { + self.is_debug == rhs.is_debug + && self.plugin_name == rhs.plugin_name + && self.available_plugins == rhs.available_plugins + && self.has_table == rhs.has_table + && self.named_column_count == rhs.named_column_count + && self.view_config == rhs.view_config + && self.drag_column == rhs.drag_column + && self.metadata == rhs.metadata + && self.open_column_settings == rhs.open_column_settings } } @@ -55,7 +82,72 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html { session, .. } = &props; - let selected_column = props.get_current_column_locator(); + + let selected_column = { + let locator = props.open_column_settings.locator.clone(); + let config = &props.view_config; + locator.filter(|locator| match locator { + ColumnLocator::Table(name) => { + locator + .name() + .map(|n| { + config.columns.iter().any(|maybe_col| { + maybe_col.as_ref().map(|col| col == n).unwrap_or_default() + }) || config.group_by.iter().any(|col| col == n) + || config.split_by.iter().any(|col| col == n) + || config.filter.iter().any(|col| col.column() == n) + || config.sort.iter().any(|col| &col.0 == n) + }) + .unwrap_or_default() + && can_render_column_styles(&props.renderer, config, &props.metadata, name) + .unwrap_or_default() + }, + _ => true, + }) + }; + + let plugin_name = props.plugin_name.clone(); + let available_plugins = props.available_plugins.clone(); + + // Dispatch callback: captures engine handles, constructs config update, renders + let on_select_plugin = { + clone!(renderer, session, presentation); + let session_metadata = props.metadata.clone(); + Callback::from(move |plugin_name: String| { + if !session.is_errored() { + let metadata = + renderer.get_next_plugin_metadata(&PluginUpdate::Update(plugin_name)); + let prev_metadata = renderer.metadata(); + let requirements = metadata.as_ref().unwrap_or(&*prev_metadata); + let rollup_features = session_metadata + .get_features() + .map(|x| x.get_group_rollup_modes()) + .unwrap(); + let group_rollups = requirements.get_group_rollups(&rollup_features); + let all_columns: Vec<_> = session_metadata + .get_table_columns() + .into_iter() + .flatten() + .cloned() + .map(Some) + .collect(); + let mut update = ViewConfigUpdate { + group_rollup_mode: group_rollups.first().cloned(), + ..ViewConfigUpdate::default() + }; + update.set_update_column_defaults(&session_metadata, &all_columns, requirements); + if session.update_view_config(update).is_ok() { + clone!(renderer, session); + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + presentation.set_open_column_settings(None); + } + }) + }; + html! { } diff --git a/rust/perspective-viewer/src/rust/components/status_bar.rs b/rust/perspective-viewer/src/rust/components/status_bar.rs index dfe52b896d..e741b6fe9b 100644 --- a/rust/perspective-viewer/src/rust/components/status_bar.rs +++ b/rust/perspective-viewer/src/rust/components/status_bar.rs @@ -22,14 +22,13 @@ use crate::components::status_bar_counter::StatusBarRowsCounter; use crate::custom_elements::copy_dropdown::*; use crate::custom_elements::export_dropdown::*; use crate::custom_events::CustomEvents; -use crate::model::*; use crate::presentation::Presentation; use crate::renderer::*; use crate::session::*; -use crate::utils::*; +use crate::tasks::*; use crate::*; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct StatusBarProps { // DOM Attribute pub id: String, @@ -41,6 +40,25 @@ pub struct StatusBarProps { #[prop_or_default] pub on_settings: Option>, + // Value props threaded from the root's `SessionProps`. + // Using these avoids PubSub subscriptions for table_loaded / table_errored. + pub has_table: bool, + pub is_errored: bool, + pub stats: Option, + /// In-flight render counter and full error, threaded to `StatusIndicator`. + pub update_count: u32, + pub error: Option, + /// Title string from session — threaded to avoid title_changed + /// subscription. + pub title: Option, + /// Theme state from presentation — threaded to avoid theme_config_updated / + /// visibility_changed subscriptions. + pub is_settings_open: bool, + pub selected_theme: Option, + pub available_themes: Rc>, + /// Whether this viewer is hosted inside a ``. + pub is_workspace: bool, + // State pub custom_events: CustomEvents, pub session: Session, @@ -51,6 +69,48 @@ pub struct StatusBarProps { impl PartialEq for StatusBarProps { fn eq(&self, other: &Self) -> bool { self.id == other.id + && self.has_table == other.has_table + && self.is_errored == other.is_errored + && self.stats == other.stats + && self.update_count == other.update_count + && self.error == other.error + && self.title == other.title + && self.is_settings_open == other.is_settings_open + && self.selected_theme == other.selected_theme + && self.available_themes == other.available_themes + && self.is_workspace == other.is_workspace + } +} + +impl HasCustomEvents for StatusBarProps { + fn custom_events(&self) -> &CustomEvents { + &self.custom_events + } +} + +impl HasPresentation for StatusBarProps { + fn presentation(&self) -> &Presentation { + &self.presentation + } +} + +impl HasRenderer for StatusBarProps { + fn renderer(&self) -> &Renderer { + &self.renderer + } +} + +impl HasSession for StatusBarProps { + fn session(&self) -> &Session { + &self.session + } +} + +impl StateProvider for StatusBarProps { + type State = StatusBarProps; + + fn clone_state(&self) -> Self::State { + self.clone() } } @@ -60,7 +120,6 @@ pub enum StatusBarMsg { Copy, Noop, Eject, - SetThemeConfig((Rc>, Option)), SetTheme(String), ResetTheme, PointerEvent(web_sys::PointerEvent), @@ -70,13 +129,13 @@ pub enum StatusBarMsg { /// A toolbar with buttons, and `Table` & `View` status information. pub struct StatusBar { - _subscriptions: [Subscription; 5], copy_ref: NodeRef, export_ref: NodeRef, input_ref: NodeRef, statusbar_ref: NodeRef, - theme: Option, - themes: Rc>, + /// Local title tracks the live `` value before the user commits the + /// change (blur / Enter). Reset to the prop value whenever the prop + /// changes. title: Option, } @@ -85,21 +144,24 @@ impl Component for StatusBar { type Properties = StatusBarProps; fn create(ctx: &Context) -> Self { - fetch_initial_theme(ctx); Self { - _subscriptions: register_listeners(ctx), copy_ref: NodeRef::default(), export_ref: NodeRef::default(), input_ref: NodeRef::default(), statusbar_ref: NodeRef::default(), - theme: None, - themes: vec![].into(), - title: ctx.props().session().get_title().clone(), + title: ctx.props().title.clone(), } } - fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { - self._subscriptions = register_listeners(ctx); + fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { + // Keep the local title in sync with the prop whenever the session title + // changes externally (e.g. restore() call) or the settings panel opens / + // closes (which resets the input element). + if ctx.props().title != old_props.title + || ctx.props().is_settings_open != old_props.is_settings_open + { + self.title = ctx.props().title.clone(); + } true } @@ -111,27 +173,24 @@ impl Component for StatusBar { false }, StatusBarMsg::ResetTheme => { - let state = ctx.props().clone_state(); + let presentation = ctx.props().presentation.clone(); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); ApiFuture::spawn(async move { - state.presentation.reset_theme().await?; - let view = state.session.get_view().into_apierror()?; - state.renderer.restyle_all(&view).await + presentation.reset_theme().await?; + let view = session.get_view().into_apierror()?; + renderer.restyle_all(&view).await }); true }, - StatusBarMsg::SetThemeConfig((themes, index)) => { - let new_theme = index.and_then(|x| themes.get(x)).cloned(); - let should_render = new_theme != self.theme || self.themes != themes; - self.theme = new_theme; - self.themes = themes; - should_render - }, StatusBarMsg::SetTheme(theme_name) => { - let state = ctx.props().clone_state(); + let presentation = ctx.props().presentation.clone(); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); ApiFuture::spawn(async move { - state.presentation.set_theme_name(Some(&theme_name)).await?; - let view = state.session.get_view().into_apierror()?; - state.renderer.restyle_all(&view).await + presentation.set_theme_name(Some(&theme_name)).await?; + let view = session.get_view().into_apierror()?; + renderer.restyle_all(&view).await }); false @@ -151,7 +210,7 @@ impl Component for StatusBar { false }, StatusBarMsg::Noop => { - self.title = ctx.props().session().get_title(); + self.title = ctx.props().title.clone(); true }, StatusBarMsg::TitleInputEvent => { @@ -201,16 +260,21 @@ impl Component for StatusBar { .. } = ctx.props(); + let has_table = ctx.props().has_table; + let is_errored = ctx.props().is_errored; + let is_settings_open = ctx.props().is_settings_open; + let title = &ctx.props().title; + let mut is_updating_class_name = classes!(); - if session.get_title().is_some() { + if title.is_some() { is_updating_class_name.push("titled"); }; - if !presentation.is_settings_open() { + if !is_settings_open { is_updating_class_name.push(["settings-closed", "titled"]); }; - if !session.has_table() { + if !has_table { is_updating_class_name.push("updating"); } @@ -229,18 +293,18 @@ impl Component for StatusBar { .link() .callback(|_: InputEvent| StatusBarMsg::TitleInputEvent); - let is_menu = session.has_table() && ctx.props().on_settings.as_ref().is_none(); + let is_menu = has_table && ctx.props().on_settings.as_ref().is_none(); let is_title = is_menu - || presentation.get_is_workspace() - || session.get_title().is_some() - || session.is_errored() + || ctx.props().is_workspace + || title.is_some() + || is_errored || presentation.is_active(&self.input_ref.cast::()); - let is_settings = session.get_title().is_some() - || presentation.get_is_workspace() - || !session.has_table() - || session.is_errored() - || presentation.is_settings_open() + let is_settings = title.is_some() + || ctx.props().is_workspace + || !has_table + || is_errored + || is_settings_open || presentation.is_active(&self.input_ref.cast::()); if is_settings { @@ -253,7 +317,15 @@ impl Component for StatusBar { class={is_updating_class_name} {onpointerdown} > - + if is_title {