diff --git a/ui/.gitignore b/ui/.gitignore
index a2b9fce842..8660270065 100644
--- a/ui/.gitignore
+++ b/ui/.gitignore
@@ -43,4 +43,9 @@ package-lock.json
vendor/jsondiffpatch.umd.js
vendor/htmlformatter.umd.js
-
+# Playwright
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+/playwright/.auth/
diff --git a/ui/app/components/disabled-plugin-card.hbs b/ui/app/components/disabled-plugin-card.hbs
index 4cb5fa17b4..ddd9be6e67 100644
--- a/ui/app/components/disabled-plugin-card.hbs
+++ b/ui/app/components/disabled-plugin-card.hbs
@@ -8,7 +8,7 @@
@level="base"
@background="neutral-secondary"
@hasBorder={{false}}
- @ariaLabel="{{@type.displayName}} - disabled engine type"
+ aria-label="{{@type.displayName}} - disabled engine type"
tabindex="0"
{{on "click" (fn @handleDisabledPluginClick @type)}}
{{on "keydown" (fn @handleDisabledPluginKeyDown @type)}}
diff --git a/ui/app/components/enabled-plugin-card.hbs b/ui/app/components/enabled-plugin-card.hbs
index 1195f71798..c4608c423f 100644
--- a/ui/app/components/enabled-plugin-card.hbs
+++ b/ui/app/components/enabled-plugin-card.hbs
@@ -11,7 +11,7 @@
@level={{if isDisabled "base" "mid"}}
@background={{if isDisabled "neutral-secondary" "neutral-primary"}}
@hasBorder={{true}}
- @ariaLabel="{{@type.displayName}} - {{if isDisabled 'disabled' 'enabled'}} engine type"
+ aria-label="{{@type.displayName}} - {{if isDisabled 'disabled' 'enabled'}} engine type"
tabindex="0"
{{on "click" this.handleSelection}}
{{on "keydown" this.handleKeyDown}}
diff --git a/ui/app/templates/vault/cluster/init.hbs b/ui/app/templates/vault/cluster/init.hbs
index 801b5784db..d667f31544 100644
--- a/ui/app/templates/vault/cluster/init.hbs
+++ b/ui/app/templates/vault/cluster/init.hbs
@@ -132,7 +132,7 @@
({
+ userType: 'superuser',
+});
+
+// setup will run once before all tests
+setup('initialize vault and setup user for testing', async ({ page, userType }) => {
+ // on fresh app load navigating to the root will land us on the initialize page
+ await page.goto('./');
+ // initialize vault
+ await page.getByRole('spinbutton', { name: 'Key shares' }).fill('1');
+ await page.getByRole('spinbutton', { name: 'Key threshold' }).fill('1');
+ await page.getByRole('button', { name: 'Initialize' }).click();
+ // listen for download event so we can get the unseal key and root token
+ const downloadPromise = page.waitForEvent('download');
+ await page.getByRole('button', { name: 'Download keys' }).click();
+ const download = await downloadPromise;
+ const keysPath = path.join(__dirname, `/tmp/${userType}-keys.json`);
+ await download.saveAs(keysPath);
+ const { keys, root_token } = JSON.parse(fs.readFileSync(keysPath, 'utf-8'));
+ // unseal vault
+ await page.getByRole('link', { name: 'Continue to Unseal' }).click();
+ await page.getByRole('textbox', { name: 'Unseal Key Portion' }).fill(keys[0]);
+ await page.getByRole('button', { name: 'Unseal' }).click();
+ // use the root token to login
+ await page.getByRole('textbox', { name: 'Token' }).fill(root_token);
+ await page.getByRole('button', { name: 'Sign in' }).click();
+ // create a policy for a specific user persona
+ // defaults to superuser but should be passed in via the project config in playwright.config.ts
+ await page.getByRole('link', { name: 'Access', exact: true }).click();
+ await page.getByRole('link', { name: 'Create ACL policy' }).click();
+ await page.getByRole('textbox', { name: 'Policy name' }).fill(userType);
+ await page.getByRole('radio', { name: 'Code editor' }).check();
+ await page.getByRole('textbox', { name: 'Policy editor' }).fill(USER_POLICY_MAP[userType]);
+ await page.getByRole('button', { name: 'Create policy' }).click();
+ // there is no UI workflow for creating tokens with specific policies
+ // generate a token using the web REPL and assign the new policy to it
+ await page.getByRole('button', { name: 'Console toggle' }).click();
+ await page
+ .getByRole('textbox', { name: 'web R.E.P.L.' })
+ .fill(`write -field=client_token auth/token/create policies=${userType} ttl=1d`);
+ await page.getByRole('textbox', { name: 'web R.E.P.L.' }).press('Enter');
+ const newToken = await page.locator('.console-ui-output pre').innerText();
+ await page.getByRole('button', { name: 'Console toggle' }).click();
+ // log out with the root token and log in with the new token/policy
+ await page.getByRole('button', { name: 'User menu' }).click();
+ await page.getByRole('link', { name: 'Log out' }).click();
+ await page.getByRole('textbox', { name: 'Token' }).fill(newToken);
+ await page.getByRole('button', { name: 'Sign in' }).click();
+ // wait for the dashboard to load to ensure login was successful
+ await page.waitForURL('**/dashboard');
+ // save the authenticated state to file
+ // subsequent tests can then reuse this session data
+ await page.context().storageState({ path: path.join(__dirname, `/tmp/${userType}-session.json`) });
+});
diff --git a/ui/e2e/policies/index.ts b/ui/e2e/policies/index.ts
new file mode 100644
index 0000000000..b449bd0574
--- /dev/null
+++ b/ui/e2e/policies/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+const readFile = (filePath: string) => {
+ return fs.readFileSync(path.join(__dirname, filePath), 'utf-8');
+};
+
+export const USER_POLICY_MAP = {
+ superuser: readFile('./superuser.hcl'),
+};
diff --git a/ui/e2e/policies/superuser.hcl b/ui/e2e/policies/superuser.hcl
new file mode 100644
index 0000000000..2c64d21233
--- /dev/null
+++ b/ui/e2e/policies/superuser.hcl
@@ -0,0 +1,6 @@
+# Copyright IBM Corp. 2016, 2025
+# SPDX-License-Identifier: BUSL-1.1
+
+path "*" {
+ capabilities = ["create", "read", "update", "delete", "list", "sudo"]
+}
\ No newline at end of file
diff --git a/ui/e2e/tests/superuser/kv.spec.ts b/ui/e2e/tests/superuser/kv.spec.ts
new file mode 100644
index 0000000000..f53b4d5c91
--- /dev/null
+++ b/ui/e2e/tests/superuser/kv.spec.ts
@@ -0,0 +1,120 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { test, expect } from '@playwright/test';
+
+test('kvv2 workflow', async ({ page }) => {
+ await page.goto('dashboard');
+ // enable kv secrets engine
+ await page.getByRole('link', { name: 'Secrets Engines' }).click();
+ await page.getByRole('link', { name: 'Enable new engine' }).click();
+ await page.locator('div').filter({ hasText: 'KV' }).nth(4).click();
+ await page.getByRole('textbox', { name: 'Path' }).click();
+ await page.getByRole('textbox', { name: 'Path' }).fill('kv-test');
+ await page.getByRole('button', { name: 'Enable engine' }).click();
+ // once enabled it should navigate to the secrets engine overview page
+ await expect(page.locator('section')).toContainText('kv-test version 2');
+ await expect(page.locator('section')).toContainText(
+ 'No secrets yet When created, secrets will be listed here. Create a secret to get started.'
+ );
+ // verify that the kv engine appears in the list view
+ await page.getByRole('link', { name: 'Secrets Engines' }).click();
+ await page.getByRole('link', { name: 'kv-test/' }).click();
+ // create a secret
+ await page.getByRole('link', { name: 'Create secret' }).click();
+ await page.getByRole('textbox', { name: 'Path for this secret' }).fill('foo');
+ await page.getByRole('textbox', { name: 'key' }).fill('bar');
+ await page.getByRole('textbox', { name: 'bar' }).fill('baz');
+ await page.getByRole('button', { name: 'Save' }).click();
+ // it should navigate to the overview page for the new secret
+ await expect(page.locator('section')).toContainText('foo');
+ await expect(page.locator('section')).toContainText(
+ 'Current version Create new The current version of this secret. 1'
+ );
+ // verify secret details
+ await page.getByRole('link', { name: 'Secret', exact: true }).click();
+ await expect(page.locator('section')).toContainText('bar');
+ await page.getByRole('button', { name: 'show value' }).click();
+ await expect(page.locator('pre')).toContainText('baz');
+ await page.locator('label').click();
+ await expect(page.getByRole('code')).toContainText('{ "bar": "baz" }');
+ // create metadata for the secret
+ await page.getByRole('link', { name: 'Metadata', exact: true }).click();
+ await expect(page.locator('#app-main-content')).toContainText(
+ 'No custom metadata This data is version-agnostic and is usually used to describe the secret being stored. Add metadata'
+ );
+ await page.getByRole('link', { name: 'Edit metadata' }).click();
+ await page.getByRole('textbox', { name: 'key' }).fill('meta');
+ await page.getByRole('textbox', { name: 'value' }).fill('data');
+ await page.getByRole('button', { name: 'Update' }).click();
+ await expect(page.locator('#app-main-content')).toContainText('meta data');
+ // create new version
+ await page.getByRole('link', { name: 'Version History' }).click();
+ await expect(page.locator('section')).toContainText('Version 1');
+ await expect(page.locator('section')).toContainText('Current');
+ await page.getByRole('button', { name: 'Manage version' }).click();
+ await page.getByRole('link', { name: 'Create new version from 1', exact: true }).click();
+ await page.getByRole('textbox', { name: 'key' }).first().fill('bar-v2');
+ await page.getByRole('textbox', { name: 'bar-v2' }).fill('baz-v2');
+ await page.getByRole('button', { name: 'Save' }).click();
+ await expect(page.locator('section')).toContainText(
+ 'Current version Create new The current version of this secret. 2'
+ );
+ await page.getByRole('link', { name: 'Version History' }).click();
+ await expect(page.locator('section')).toContainText('Version 2');
+ await expect(page.locator('section')).toContainText('Current');
+ await page.getByRole('link', { name: 'Version diff' }).click();
+ await expect(page.locator('section')).toContainText('bar"baz"bar-v2"baz-v2"');
+ // delete version 2
+ await page.goto('secrets-engines/kv-test/kv/foo');
+ await page.getByRole('link', { name: 'Secret', exact: true }).click();
+ await page.getByRole('button', { name: 'Delete' }).click();
+ await page.getByRole('radio', { name: 'Delete this version This' }).check();
+ await page.getByRole('button', { name: 'Confirm' }).click();
+ await expect(page.locator('section')).toContainText(
+ 'Current version Deleted Create new The current version of this secret was deleted'
+ );
+ await page.getByRole('link', { name: 'Secret', exact: true }).click();
+ await expect(page.locator('section')).toContainText(
+ 'Version 2 of this secret has been deleted This version has been deleted but can be undeleted. View other versions of this secret by clicking the Version History tab above. KV v2 API docs'
+ );
+ // undelete version
+ await page.getByRole('button', { name: 'Undelete' }).click();
+ await expect(page.locator('section')).toContainText(
+ 'Current version Create new The current version of this secret. 2'
+ );
+ // delete latest version
+ await page.getByRole('link', { name: 'Secret', exact: true }).click();
+ await page.getByRole('button', { name: 'Version' }).click();
+ await page.getByRole('link', { name: 'Version 1' }).click();
+ await page.getByRole('button', { name: 'Delete' }).click();
+ await page.getByRole('radio', { name: 'Delete latest version This' }).check();
+ await page.getByRole('button', { name: 'Confirm' }).click();
+ await expect(page.locator('section')).toContainText(
+ 'Current version Deleted Create new The current version of this secret was deleted'
+ );
+ // destroy version 2
+ await page.getByRole('link', { name: 'Secret', exact: true }).click();
+ await page.getByRole('button', { name: 'Destroy' }).click();
+ await page.getByRole('button', { name: 'Confirm' }).click();
+ await expect(page.locator('section')).toContainText(
+ 'Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. 2'
+ );
+ await page.getByRole('link', { name: 'Secret', exact: true }).click();
+ await expect(page.locator('section')).toContainText(
+ 'Version 2 of this secret has been permanently destroyed A version that has been permanently deleted cannot be restored. You can view other versions of this secret in the Version History tab above. KV v2 API docs'
+ );
+ // destroy version 1
+ await page.getByRole('button', { name: 'Version' }).click();
+ await page.getByRole('link', { name: 'Version 1' }).click();
+ await page.getByRole('button', { name: 'Destroy' }).click();
+ await page.getByRole('button', { name: 'Confirm' }).click();
+ await page.getByRole('link', { name: 'Secret', exact: true }).click();
+ await page.getByRole('button', { name: 'Version' }).click();
+ await page.getByRole('link', { name: 'Version 1' }).click();
+ await expect(page.locator('section')).toContainText(
+ 'Version 1 of this secret has been permanently destroyed A version that has been permanently deleted cannot be restored. You can view other versions of this secret in the Version History tab above. KV v2 API docs'
+ );
+});
diff --git a/ui/e2e/vault-config.json b/ui/e2e/vault-config.json
new file mode 100644
index 0000000000..1e49d7de7b
--- /dev/null
+++ b/ui/e2e/vault-config.json
@@ -0,0 +1,15 @@
+{
+ "ui": true,
+ "disable_mlock": true,
+
+ "storage": {
+ "inmem": {}
+ },
+
+ "listener": {
+ "tcp": {
+ "address": "127.0.0.1:8204",
+ "tls_disable": 1
+ }
+ }
+}
diff --git a/ui/package.json b/ui/package.json
index afbf41c071..4225a942b4 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -42,7 +42,8 @@
"test:server": "node scripts/start-vault.js --server",
"test:dev": "node scripts/start-vault.js",
"vault": "VAULT_REDIRECT_ADDR=http://127.0.0.1:8200 vault server -log-level=error -dev -dev-root-token-id=root -dev-ha -dev-transactional",
- "vault:cluster": "VAULT_REDIRECT_ADDR=http://127.0.0.1:8202 vault server -log-level=error -dev -dev-root-token-id=root -dev-listen-address=127.0.0.1:8202 -dev-ha -dev-transactional"
+ "vault:cluster": "VAULT_REDIRECT_ADDR=http://127.0.0.1:8202 vault server -log-level=error -dev -dev-root-token-id=root -dev-listen-address=127.0.0.1:8202 -dev-ha -dev-transactional",
+ "vault:e2e": "vault server"
},
"devDependencies": {
"@babel/cli": "~7.27.0",
@@ -67,9 +68,11 @@
"@glint/template": "^1.7.3",
"@icholy/duration": "~5.1.0",
"@lineal-viz/lineal": "~0.5.1",
+ "@playwright/test": "^1.58.0",
"@tsconfig/ember": "~2.0.0",
"@types/d3-array": "~3.2.1",
"@types/ember-data": "~4.4.16",
+ "@types/node": "^25.1.0",
"@types/qunit": "~2.19.12",
"@types/rsvp": "~4.0.9",
"@types/shell-quote": "~1.7.5",
diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts
new file mode 100644
index 0000000000..75529c9e67
--- /dev/null
+++ b/ui/playwright.config.ts
@@ -0,0 +1,93 @@
+/**
+ * Copyright IBM Corp. 2016, 2025
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { defineConfig, devices } from '@playwright/test';
+import path from 'path';
+import fs from 'fs';
+import { USER_POLICY_MAP } from './e2e/policies';
+
+import type { UserSetupOptions } from './e2e/init.setup';
+
+const userTypes = Object.keys(USER_POLICY_MAP);
+
+// start at port 8204 and increment for each project to allow them to run concurrently
+const getURL = (increment: number, server = false) => {
+ const port = `820${4 + increment}`;
+ return server ? `127.0.0.1:${port}` : `http://localhost:${port}/ui/vault/`;
+};
+
+// create tmp dir if it doesn't exist for storing session, keys and vault config files
+const tmpDir = path.join(__dirname, '/e2e/tmp');
+fs.mkdirSync(tmpDir, { recursive: true });
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './e2e',
+ // opt out of parallel execution with a test file - by default tests will run in the order they are defined
+ fullyParallel: false,
+ // fail the build on CI if you accidentally left test.only in the source code.
+ forbidOnly: !!process.env.CI,
+ // retry on CI only
+ retries: process.env.CI ? 2 : 0,
+ // use a worker for each project so they run concurrently
+ workers: userTypes.length,
+ // reporter to use. See https://playwright.dev/docs/test-reporters
+ reporter: 'html',
+ // shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions.
+ use: {
+ // collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
+ trace: 'on-first-retry',
+ },
+ projects: [
+ // create setup project for each user type
+ ...userTypes.map((userType, index) => ({
+ name: `setup:${userType}`,
+ testMatch: /init\.setup\.ts/,
+ use: {
+ userType,
+ baseURL: getURL(index),
+ },
+ })),
+ // create browser projects for each user type
+ ...userTypes.map((userType, index) => {
+ const sessionFile = path.join(tmpDir, `${userType}-session.json`);
+ return {
+ name: `chrome:${userType}`,
+ dependencies: [`setup:${userType}`],
+ workers: 1,
+ // only run tests for this user type
+ testDir: `./e2e/tests/${userType}`,
+ use: {
+ ...devices['Desktop Chrome'],
+ // only use if file has already been created by the setup project
+ storageState: fs.existsSync(sessionFile) ? sessionFile : undefined,
+ // start at port 8204 and increment for each project to allow them to run concurrently without conflicts
+ baseURL: getURL(index),
+ },
+ };
+ }),
+ ],
+ webServer: [
+ // start a vault server for each project on a different port to allow them to run concurrently
+ ...userTypes.map((userType, index) => {
+ // read base config file
+ const config = JSON.parse(fs.readFileSync(path.join(__dirname, '/e2e/vault-config.json'), 'utf-8'));
+ // set the listener address with correct port for this project
+ config.listener.tcp.address = getURL(index, true);
+ // write the config to a new file for this project
+ const configPath = path.join(tmpDir, `${userType}-vault-config.json`);
+ fs.writeFileSync(configPath, JSON.stringify(config));
+
+ return {
+ // start vault server (not dev) with inmem storage
+ command: `pnpm run vault:e2e -config=${configPath}`,
+ url: getURL(index),
+ reuseExistingServer: false,
+ };
+ }),
+ ],
+});
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 1114b255c8..9aecfe284e 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -126,6 +126,9 @@ importers:
'@lineal-viz/lineal':
specifier: ~0.5.1
version: 0.5.1(@babel/core@7.26.10)(@glint/template@1.7.3)(ember-source@5.8.0(@babel/core@7.26.10)(@glimmer/component@1.1.2(@babel/core@7.26.10))(@glint/template@1.7.3)(rsvp@4.8.5)(webpack@5.94.0))
+ '@playwright/test':
+ specifier: ^1.58.0
+ version: 1.58.0
'@tsconfig/ember':
specifier: ~2.0.0
version: 2.0.0
@@ -135,6 +138,9 @@ importers:
'@types/ember-data':
specifier: ~4.4.16
version: 4.4.16(@babel/core@7.26.10)
+ '@types/node':
+ specifier: ^25.1.0
+ version: 25.1.0
'@types/qunit':
specifier: ~2.19.12
version: 2.19.12
@@ -1745,6 +1751,11 @@ packages:
resolution: {integrity: sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ '@playwright/test@1.58.0':
+ resolution: {integrity: sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@pnpm/constants@7.1.1':
resolution: {integrity: sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw==}
engines: {node: '>=16.14'}
@@ -2045,8 +2056,8 @@ packages:
'@types/minimatch@5.1.2':
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
- '@types/node@22.15.21':
- resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==}
+ '@types/node@25.1.0':
+ resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==}
'@types/prettier@2.7.3':
resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==}
@@ -4891,6 +4902,11 @@ packages:
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -6583,6 +6599,16 @@ packages:
resolution: {integrity: sha512-cjJP/mYuGyMrjJ49jI04khId5Oufd3nFTUYBzQTIIVNI7/oAWdwXEfpwTF8HELFV/gz+WGYUBHCe3KHWD8rYvg==}
engines: {node: '>=6.0.0'}
+ playwright-core@1.58.0:
+ resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.58.0:
+ resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
portfinder@1.0.37:
resolution: {integrity: sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==}
engines: {node: '>= 10.12'}
@@ -7825,8 +7851,8 @@ packages:
underscore@1.13.7:
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
- undici-types@6.21.0:
- resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
unicode-canonical-property-names-ecmascript@2.0.1:
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
@@ -10811,6 +10837,10 @@ snapshots:
'@pkgr/core@0.2.4': {}
+ '@playwright/test@1.58.0':
+ dependencies:
+ playwright: 1.58.0
+
'@pnpm/constants@7.1.1': {}
'@pnpm/error@5.0.3':
@@ -10892,7 +10922,7 @@ snapshots:
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/chai-as-promised@7.1.8':
dependencies:
@@ -10906,11 +10936,11 @@ snapshots:
'@types/connect@3.4.38':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/cors@2.8.18':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/d3-array@3.2.1': {}
@@ -11184,7 +11214,7 @@ snapshots:
'@types/express-serve-static-core@4.19.6':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@@ -11198,27 +11228,27 @@ snapshots:
'@types/fs-extra@5.1.0':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/fs-extra@8.1.5':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/fs-extra@9.0.13':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/geojson@7946.0.16': {}
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 5.1.2
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/glob@8.1.0':
dependencies:
'@types/minimatch': 5.1.2
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/http-errors@2.0.4': {}
@@ -11247,9 +11277,9 @@ snapshots:
'@types/minimatch@5.1.2': {}
- '@types/node@22.15.21':
+ '@types/node@25.1.0':
dependencies:
- undici-types: 6.21.0
+ undici-types: 7.16.0
'@types/prettier@2.7.3': {}
@@ -11262,7 +11292,7 @@ snapshots:
'@types/rimraf@2.0.5':
dependencies:
'@types/glob': 8.1.0
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/rsvp@4.0.9': {}
@@ -11271,12 +11301,12 @@ snapshots:
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
'@types/send': 0.17.4
'@types/shell-quote@1.7.5': {}
@@ -12380,7 +12410,7 @@ snapshots:
broccoli-rollup@4.0.0:
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
broccoli-plugin: 2.1.0
fs-tree-diff: 2.0.1
heimdalljs: 0.2.6
@@ -14624,7 +14654,7 @@ snapshots:
engine.io@6.6.4:
dependencies:
'@types/cors': 2.8.18
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
@@ -15414,6 +15444,9 @@ snapshots:
fs.realpath@1.0.0: {}
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -16150,7 +16183,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 25.1.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -17211,6 +17244,14 @@ snapshots:
bytestreamjs: 1.1.3
pvutils: 1.1.3
+ playwright-core@1.58.0: {}
+
+ playwright@1.58.0:
+ dependencies:
+ playwright-core: 1.58.0
+ optionalDependencies:
+ fsevents: 2.3.2
+
portfinder@1.0.37:
dependencies:
async: 2.6.4
@@ -18714,7 +18755,7 @@ snapshots:
underscore@1.13.7: {}
- undici-types@6.21.0: {}
+ undici-types@7.16.0: {}
unicode-canonical-property-names-ecmascript@2.0.1: {}