vease 1.52.0-rc.8 → 1.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -105,17 +105,19 @@ async function toggleShrinkFilter(window) {
105
105
  await window.getByTestId("shrinkFilterButton").click();
106
106
  await waitForActionSettled(window);
107
107
  }
108
- async function setShrinkFactor(window, shrinkFactorValue) {
109
- const slider = window.getByTestId("shrinkFactorSlider");
108
+ async function setSliderRatio(window, sliderTestId, ratio) {
109
+ const slider = window.getByTestId(sliderTestId);
110
110
  const box = await slider.boundingBox();
111
- if (!box) throw new Error("Could not get bounding box of the shrink factor slider.");
112
- const clickX = box.width * shrinkFactorValue;
111
+ if (!box) throw new Error(`Could not get bounding box of the ${sliderTestId} slider.`);
113
112
  await slider.click({ position: {
114
- x: clickX,
113
+ x: box.width * ratio,
115
114
  y: box.height / 2
116
115
  } });
117
116
  await waitForActionSettled(window);
118
117
  }
118
+ async function setShrinkFactor(window, shrinkFactorValue) {
119
+ await setSliderRatio(window, "shrinkFactorSlider", shrinkFactorValue);
120
+ }
119
121
  async function resetShrinkFilter(window) {
120
122
  await window.getByTestId("resetShrinkButton").click();
121
123
  await waitForActionSettled(window);
@@ -133,6 +135,21 @@ async function selectShrinkDatasets(window, datasetName, index = 0) {
133
135
  await select.click();
134
136
  await waitForActionSettled(window);
135
137
  }
138
+ async function toggleExplodeFilter(window) {
139
+ await window.getByTestId("explodeFilterButton").click();
140
+ await waitForActionSettled(window);
141
+ }
142
+ async function setExplodeFactor(window, explodeFactorValue) {
143
+ await setSliderRatio(window, "explodeFactorSlider", explodeFactorValue);
144
+ }
145
+ async function resetExplodeFilter(window) {
146
+ await window.getByTestId("resetExplodeButton").click();
147
+ await waitForActionSettled(window);
148
+ }
149
+ async function removeExplodeFilter(window) {
150
+ await window.getByTestId("removeExplodeButton").click();
151
+ await waitForActionSettled(window);
152
+ }
136
153
  async function toggleThresholdFilter(window) {
137
154
  await window.getByTestId("thresholdFilterButton").click();
138
155
  await waitForActionSettled(window);
@@ -207,4 +224,4 @@ async function drawZoomBox(window, deltaX, deltaY) {
207
224
  await expect(window.getByTestId("zoomBoxActiveChip")).toBeHidden();
208
225
  }
209
226
  //#endregion
210
- export { activateZoomToBox, clearRuler, closeCameraManager, drawZoomBox, ensureHighlightMenuOpen, removeThresholdFilter, resetCamera, resetShrinkFilter, resetThresholdFilter, restoreCameraPosition, rotateCamera, saveCameraPosition, selectCameraOrientation, selectShrinkDatasets, selectThresholdBlockPolyhedron, selectThresholdCellAttribute, setRulerPointInput, setShrinkFactor, setThresholdMinimum, setZScaling, toggleCameraManager, toggleCameraOrientation, toggleCenterOnClick, toggleGridScale, toggleRuler, toggleRulerSnap, toggleShrinkFilter, toggleShrinkTargetAllVisible, toggleThresholdFilter };
227
+ export { activateZoomToBox, clearRuler, closeCameraManager, drawZoomBox, ensureHighlightMenuOpen, removeExplodeFilter, removeThresholdFilter, resetCamera, resetExplodeFilter, resetShrinkFilter, resetThresholdFilter, restoreCameraPosition, rotateCamera, saveCameraPosition, selectCameraOrientation, selectShrinkDatasets, selectThresholdBlockPolyhedron, selectThresholdCellAttribute, setExplodeFactor, setRulerPointInput, setShrinkFactor, setThresholdMinimum, setZScaling, toggleCameraManager, toggleCameraOrientation, toggleCenterOnClick, toggleExplodeFilter, toggleGridScale, toggleRuler, toggleRulerSnap, toggleShrinkFilter, toggleShrinkTargetAllVisible, toggleThresholdFilter };
@@ -18,7 +18,7 @@ const test = test$1.extend({
18
18
  screenshotMask: [async ({}, use) => {
19
19
  await use({ locators: [] });
20
20
  }, { scope: "test" }],
21
- window: [async ({ mode, playwright, browserName, headless, channel, launchOptions }, use, workerInfo) => {
21
+ app: [async ({ mode, playwright, browserName, headless, channel, launchOptions }, use, workerInfo) => {
22
22
  const configPath = fs.mkdtempSync(path.join(os.tmpdir(), `vease-e2e-worker${workerInfo.parallelIndex}-`));
23
23
  process.env.XDG_CONFIG_HOME = configPath;
24
24
  process.env.APPDATA = configPath;
@@ -29,14 +29,30 @@ const test = test$1.extend({
29
29
  channel
30
30
  });
31
31
  }
32
- const { window, cleanup } = await navigateToApp(mode, launchBrowser);
33
- await use(window);
34
- await cleanup();
32
+ const app = {
33
+ ...await navigateToApp(mode, launchBrowser),
34
+ restart: async () => {
35
+ await app.cleanup();
36
+ const { window, cleanup, electronApp } = await navigateToApp(mode, launchBrowser);
37
+ app.window = window;
38
+ app.cleanup = cleanup;
39
+ app.electronApp = electronApp;
40
+ return window;
41
+ }
42
+ };
43
+ await use(app);
44
+ await app.cleanup();
35
45
  fs.rmSync(configPath, {
36
46
  recursive: true,
37
47
  force: true
38
48
  });
39
49
  }, { scope: "worker" }],
50
+ restartApp: [async ({ app }, use) => {
51
+ await use(app.restart);
52
+ }, { scope: "worker" }],
53
+ window: [async ({ app }, use) => {
54
+ await use(app.window);
55
+ }, { scope: "worker" }],
40
56
  logTestProgress: [async ({}, use, testInfo) => {
41
57
  const name = `${path.basename(testInfo.file)} › ${testInfo.title}`;
42
58
  consola.info(`\u001B[33m[START]\u001B[0m ${name}`);
@@ -47,8 +63,9 @@ const test = test$1.extend({
47
63
  const duration = ((Date.now() - start) / MILLISECONDS_PER_SECOND).toFixed(2);
48
64
  consola.info(`\u001B[35m[END]\u001B[0m ${name} : ${statusColor}TEST ${status}\u001B[0m (${duration}s)`);
49
65
  }, { auto: true }],
50
- autoScreenshot: [async ({ window, screenshotMask }, use, testInfo) => {
66
+ autoScreenshot: [async ({ app, screenshotMask }, use, testInfo) => {
51
67
  await use(void 0);
68
+ const { window } = app;
52
69
  if (testInfo.status === testInfo.expectedStatus) await expect(window).toHaveScreenshot({ mask: [window.getByTestId("accountNavButton"), ...screenshotMask.locators] });
53
70
  }, { auto: true }]
54
71
  });
@@ -169,6 +169,7 @@ async function navigateToApp(mode, launchBrowser) {
169
169
  await firstWindow.waitForFunction(() => document.readyState === "complete");
170
170
  return {
171
171
  window: firstWindow,
172
+ electronApp,
172
173
  cleanup: async () => {
173
174
  await electronApp.close();
174
175
  }
@@ -1,16 +1,45 @@
1
1
  import { waitForActionSettled } from "./wait_for_action_settled.js";
2
+ import { closeFeedbackSnackbar } from "./app_interaction.js";
3
+ import { expect } from "@playwright/test";
2
4
  //#region tests/e2e/utils/project_interaction.ts
3
- async function exportProject(window) {
5
+ const importProjectTimeout = 2e4;
6
+ const downloadTimeout = 3e4;
7
+ async function saveNextElectronDownload(electronApp, savePath, trigger) {
8
+ await electronApp.evaluate(({ session }, filePath) => {
9
+ globalThis.e2eDownloadState = "pending";
10
+ session.defaultSession.once("will-download", (_event, item) => {
11
+ item.setSavePath(filePath);
12
+ item.once("done", (_doneEvent, state) => {
13
+ globalThis.e2eDownloadState = state;
14
+ });
15
+ });
16
+ }, savePath);
17
+ await trigger();
18
+ async function getDownloadState() {
19
+ return await electronApp.evaluate(() => globalThis.e2eDownloadState);
20
+ }
21
+ await expect.poll(getDownloadState, { timeout: downloadTimeout }).toBe("completed");
22
+ }
23
+ async function exportProject(window, savePath, electronApp) {
4
24
  await window.getByTestId("projectMenuButton").click();
5
25
  await waitForActionSettled(window);
6
- await window.getByTestId("exportProjectButton").click();
26
+ const exportProjectButton = window.getByTestId("exportProjectButton");
27
+ if (electronApp) await saveNextElectronDownload(electronApp, savePath, async () => {
28
+ await exportProjectButton.click();
29
+ });
30
+ else {
31
+ const [download] = await Promise.all([window.waitForEvent("download"), exportProjectButton.click()]);
32
+ await download.saveAs(savePath);
33
+ }
7
34
  await waitForActionSettled(window);
35
+ await closeFeedbackSnackbar(window);
8
36
  }
9
37
  async function importProject(window, projectFilePath) {
10
38
  await window.getByTestId("projectMenuButton").click();
11
39
  await waitForActionSettled(window);
12
40
  await window.getByTestId("importProjectInput").setInputFiles(projectFilePath);
13
- await window.waitForTimeout(8e3);
41
+ await waitForActionSettled(window, importProjectTimeout);
42
+ await closeFeedbackSnackbar(window);
14
43
  }
15
44
  //#endregion
16
45
  export { exportProject, importProject };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vease",
3
- "version": "1.52.0-rc.8",
3
+ "version": "1.52.0",
4
4
  "description": "Desktop and cloud software for data visualization",
5
5
  "keywords": [
6
6
  "Desktop",
@@ -84,12 +84,12 @@
84
84
  "@ai-sdk/vue": "4.0.93",
85
85
  "@fontsource/michroma": "5.2.6",
86
86
  "@fontsource/roboto": "5.2.10",
87
- "@geode/cloud-api": "next",
88
- "@geode/opengeodeweb-back": "next",
89
- "@geode/opengeodeweb-front": "next",
90
- "@geode/opengeodeweb-viewer": "next",
91
- "@geode/vease-back": "next",
92
- "@geode/vease-viewer": "next",
87
+ "@geode/cloud-api": "latest",
88
+ "@geode/opengeodeweb-back": "latest",
89
+ "@geode/opengeodeweb-front": "latest",
90
+ "@geode/opengeodeweb-viewer": "latest",
91
+ "@geode/vease-back": "latest",
92
+ "@geode/vease-viewer": "latest",
93
93
  "@nuxtjs/mcp-toolkit": "0.18.1",
94
94
  "ai": "7.0.93",
95
95
  "bowser": "2.14.1",
@@ -15,7 +15,6 @@ async function resetCamera(window: Page): Promise<void> {
15
15
  }
16
16
 
17
17
  const ROTATE_DRAG_STEPS = 20;
18
-
19
18
  // Vtk.js' RenderWindowInteractor turns a pointermove arriving more than 200ms after the previous one into a StartMouseMove, which InteractorStyleTrackballCamera ignores.
20
19
  // A skipped move mid-drag is caught up by the next handled one (rotation uses the delta from the last handled position), but a skipped final move is lost for good.
21
20
  // On slow runners (Windows + swiftshader) the last step regularly lands late, leaving the camera one step short of the requested angle.
@@ -145,17 +144,20 @@ async function toggleShrinkFilter(window: Page): Promise<void> {
145
144
  await waitForActionSettled(window);
146
145
  }
147
146
 
148
- async function setShrinkFactor(window: Page, shrinkFactorValue: number): Promise<void> {
149
- const slider = window.getByTestId("shrinkFactorSlider");
147
+ async function setSliderRatio(window: Page, sliderTestId: string, ratio: number): Promise<void> {
148
+ const slider = window.getByTestId(sliderTestId);
150
149
  const box = await slider.boundingBox();
151
150
  if (!box) {
152
- throw new Error("Could not get bounding box of the shrink factor slider.");
151
+ throw new Error(`Could not get bounding box of the ${sliderTestId} slider.`);
153
152
  }
154
- const clickX = box.width * shrinkFactorValue;
155
- await slider.click({ position: { x: clickX, y: box.height / 2 } });
153
+ await slider.click({ position: { x: box.width * ratio, y: box.height / 2 } });
156
154
  await waitForActionSettled(window);
157
155
  }
158
156
 
157
+ async function setShrinkFactor(window: Page, shrinkFactorValue: number): Promise<void> {
158
+ await setSliderRatio(window, "shrinkFactorSlider", shrinkFactorValue);
159
+ }
160
+
159
161
  async function resetShrinkFilter(window: Page): Promise<void> {
160
162
  await window.getByTestId("resetShrinkButton").click();
161
163
  await waitForActionSettled(window);
@@ -179,6 +181,25 @@ async function selectShrinkDatasets(window: Page, datasetName: string, index = 0
179
181
  await waitForActionSettled(window);
180
182
  }
181
183
 
184
+ async function toggleExplodeFilter(window: Page): Promise<void> {
185
+ await window.getByTestId("explodeFilterButton").click();
186
+ await waitForActionSettled(window);
187
+ }
188
+
189
+ async function setExplodeFactor(window: Page, explodeFactorValue: number): Promise<void> {
190
+ await setSliderRatio(window, "explodeFactorSlider", explodeFactorValue);
191
+ }
192
+
193
+ async function resetExplodeFilter(window: Page): Promise<void> {
194
+ await window.getByTestId("resetExplodeButton").click();
195
+ await waitForActionSettled(window);
196
+ }
197
+
198
+ async function removeExplodeFilter(window: Page): Promise<void> {
199
+ await window.getByTestId("removeExplodeButton").click();
200
+ await waitForActionSettled(window);
201
+ }
202
+
182
203
  async function toggleThresholdFilter(window: Page): Promise<void> {
183
204
  await window.getByTestId("thresholdFilterButton").click();
184
205
  await waitForActionSettled(window);
@@ -308,6 +329,10 @@ export {
308
329
  resetShrinkFilter,
309
330
  toggleShrinkTargetAllVisible,
310
331
  selectShrinkDatasets,
332
+ toggleExplodeFilter,
333
+ setExplodeFactor,
334
+ resetExplodeFilter,
335
+ removeExplodeFilter,
311
336
  toggleThresholdFilter,
312
337
  selectThresholdCellAttribute,
313
338
  selectThresholdBlockPolyhedron,
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import type { Browser, Locator, Page } from "@playwright/test";
8
8
  // oxlint-disable-next-line eslint/no-duplicate-imports
9
9
  import { test as base, expect } from "@playwright/test";
10
+ import type { ElectronApplication } from "playwright";
10
11
  import { consola } from "consola";
11
12
 
12
13
  // Local imports
@@ -18,9 +19,19 @@ interface ScreenshotMask {
18
19
  locators: Locator[];
19
20
  }
20
21
 
22
+ interface RunningApp {
23
+ window: Page;
24
+ cleanup: () => Promise<void>;
25
+ // Only set in DESKTOP mode
26
+ electronApp?: ElectronApplication;
27
+ restart: () => Promise<Page>;
28
+ }
29
+
21
30
  interface WorkerFixtures {
22
31
  mode: string;
23
32
  suiteId: string;
33
+ app: RunningApp;
34
+ restartApp: () => Promise<Page>;
24
35
  window: Page;
25
36
  }
26
37
 
@@ -43,7 +54,8 @@ const test = base.extend<TestFixtures, WorkerFixtures>({
43
54
  { scope: "test" },
44
55
  ],
45
56
 
46
- window: [
57
+ // Mutable so restartApp can swap in a freshly launched app
58
+ app: [
47
59
  async (
48
60
  { mode, playwright, browserName, headless, channel, launchOptions },
49
61
  use,
@@ -64,14 +76,40 @@ const test = base.extend<TestFixtures, WorkerFixtures>({
64
76
  });
65
77
  return browser;
66
78
  }
67
- const { window, cleanup } = await navigateToApp(mode, launchBrowser);
68
- await use(window);
69
- await cleanup();
79
+ const app: RunningApp = {
80
+ ...(await navigateToApp(mode, launchBrowser)),
81
+ restart: async (): Promise<Page> => {
82
+ await app.cleanup();
83
+ const { window, cleanup, electronApp } = await navigateToApp(mode, launchBrowser);
84
+ app.window = window;
85
+ app.cleanup = cleanup;
86
+ app.electronApp = electronApp;
87
+ return window;
88
+ },
89
+ };
90
+ await use(app);
91
+ await app.cleanup();
70
92
  fs.rmSync(configPath, { recursive: true, force: true });
71
93
  },
72
94
  { scope: "worker" },
73
95
  ],
74
96
 
97
+ // Kills the running app and launches a new one, like a user closing and reopening it
98
+ restartApp: [
99
+ async ({ app }, use): Promise<void> => {
100
+ await use(app.restart);
101
+ },
102
+ { scope: "worker" },
103
+ ],
104
+
105
+ // Page of the app launched with the worker: after restartApp, use the returned page or app.window
106
+ window: [
107
+ async ({ app }, use): Promise<void> => {
108
+ await use(app.window);
109
+ },
110
+ { scope: "worker" },
111
+ ],
112
+
75
113
  logTestProgress: [
76
114
  // oxlint-disable-next-line no-empty-pattern
77
115
  async ({}, use, testInfo): Promise<void> => {
@@ -90,8 +128,10 @@ const test = base.extend<TestFixtures, WorkerFixtures>({
90
128
  ],
91
129
 
92
130
  autoScreenshot: [
93
- async ({ window, screenshotMask }, use, testInfo): Promise<void> => {
131
+ async ({ app, screenshotMask }, use, testInfo): Promise<void> => {
94
132
  await use(undefined);
133
+ // Read at teardown: the test may have restarted the app
134
+ const { window } = app;
95
135
  if (testInfo.status === testInfo.expectedStatus) {
96
136
  // The account icon depends on login state (only cloud is logged in)
97
137
  await expect(window).toHaveScreenshot({
@@ -218,7 +218,7 @@ async function newBrowserPage(browser: Browser): Promise<Page> {
218
218
  async function navigateToApp(
219
219
  mode: string,
220
220
  launchBrowser: () => Promise<Browser>,
221
- ): Promise<{ window: Page; cleanup: () => Promise<void> }> {
221
+ ): Promise<{ window: Page; cleanup: () => Promise<void>; electronApp?: ElectronApplication }> {
222
222
  consola.info(`Testing app in ${mode} mode`);
223
223
  // The desktop app is driven through Electron: no browser is launched
224
224
  if (mode === "DESKTOP") {
@@ -227,6 +227,7 @@ async function navigateToApp(
227
227
  await firstWindow.waitForFunction(() => document.readyState === "complete");
228
228
  return {
229
229
  window: firstWindow,
230
+ electronApp,
230
231
  cleanup: async () => {
231
232
  await electronApp.close();
232
233
  },
@@ -1,11 +1,58 @@
1
- import type { Page } from "@playwright/test";
1
+ import { type Page, expect } from "@playwright/test";
2
+ import type { ElectronApplication } from "playwright";
3
+ import { closeFeedbackSnackbar } from "./app_interaction";
2
4
  import { waitForActionSettled } from "./wait_for_action_settled";
3
5
 
4
- async function exportProject(window: Page): Promise<void> {
6
+ const importProjectTimeout = 20_000;
7
+ const downloadTimeout = 30_000;
8
+
9
+ declare global {
10
+ var e2eDownloadState: string | undefined;
11
+ }
12
+
13
+ async function saveNextElectronDownload(
14
+ electronApp: ElectronApplication,
15
+ savePath: string,
16
+ trigger: () => Promise<void>,
17
+ ): Promise<void> {
18
+ await electronApp.evaluate(({ session }, filePath) => {
19
+ globalThis.e2eDownloadState = "pending";
20
+ session.defaultSession.once("will-download", (_event, item) => {
21
+ item.setSavePath(filePath);
22
+ item.once("done", (_doneEvent, state) => {
23
+ globalThis.e2eDownloadState = state;
24
+ });
25
+ });
26
+ }, savePath);
27
+ await trigger();
28
+ async function getDownloadState(): Promise<string | undefined> {
29
+ const state = await electronApp.evaluate(() => globalThis.e2eDownloadState);
30
+ return state;
31
+ }
32
+ await expect.poll(getDownloadState, { timeout: downloadTimeout }).toBe("completed");
33
+ }
34
+
35
+ async function exportProject(
36
+ window: Page,
37
+ savePath: string,
38
+ electronApp?: ElectronApplication,
39
+ ): Promise<void> {
5
40
  await window.getByTestId("projectMenuButton").click();
6
41
  await waitForActionSettled(window);
7
- await window.getByTestId("exportProjectButton").click();
42
+ const exportProjectButton = window.getByTestId("exportProjectButton");
43
+ if (electronApp) {
44
+ await saveNextElectronDownload(electronApp, savePath, async () => {
45
+ await exportProjectButton.click();
46
+ });
47
+ } else {
48
+ const [download] = await Promise.all([
49
+ window.waitForEvent("download"),
50
+ exportProjectButton.click(),
51
+ ]);
52
+ await download.saveAs(savePath);
53
+ }
8
54
  await waitForActionSettled(window);
55
+ await closeFeedbackSnackbar(window);
9
56
  }
10
57
 
11
58
  async function importProject(window: Page, projectFilePath: string): Promise<void> {
@@ -13,8 +60,8 @@ async function importProject(window: Page, projectFilePath: string): Promise<voi
13
60
  await waitForActionSettled(window);
14
61
  const fileInput = window.getByTestId("importProjectInput");
15
62
  await fileInput.setInputFiles(projectFilePath);
16
- const importTimeout = 8000;
17
- await window.waitForTimeout(importTimeout);
63
+ await waitForActionSettled(window, importProjectTimeout);
64
+ await closeFeedbackSnackbar(window);
18
65
  }
19
66
 
20
67
  export { exportProject, importProject };