diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index b437b0d023..b3d5b0ab2a 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -100,6 +100,7 @@ jobs:
id: findPr
- name: Post or Update Coverage Summary Comment
+ if: ${{ steps.findPr.outputs.number != '' }}
uses: actions/github-script@v7
with:
script: |
diff --git a/.github/workflows/tests-e2e-approve.yml b/.github/workflows/tests-e2e-approve.yml
new file mode 100644
index 0000000000..8976a56040
--- /dev/null
+++ b/.github/workflows/tests-e2e-approve.yml
@@ -0,0 +1,15 @@
+name: ✅ E2E Approve
+
+on:
+ pull_request_review:
+ types: [submitted]
+
+jobs:
+ e2e-approve:
+ runs-on: ubuntu-latest
+ if: github.event.review.state == 'approved'
+ steps:
+ - name: Add "e2e-approved" Label
+ uses: actions-ecosystem/action-add-labels@v1
+ with:
+ labels: e2e-approved
diff --git a/.github/workflows/tests-e2e-docker.yml b/.github/workflows/tests-e2e-docker.yml
index ece239ee49..b0b0e72297 100644
--- a/.github/workflows/tests-e2e-docker.yml
+++ b/.github/workflows/tests-e2e-docker.yml
@@ -83,7 +83,7 @@ jobs:
TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \
RI_SERVER_TLS_CERT="$RI_SERVER_TLS_CERT" \
RI_SERVER_TLS_KEY="$RI_SERVER_TLS_KEY" \
- docker compose \
+ docker compose --profile e2e \
-f tests/e2e/rte.docker-compose.yml \
-f tests/e2e/docker.web.docker-compose.yml \
up --abort-on-container-exit --force-recreate
diff --git a/.github/workflows/tests-e2e-playwright.yml b/.github/workflows/tests-e2e-playwright.yml
new file mode 100644
index 0000000000..972e3cce62
--- /dev/null
+++ b/.github/workflows/tests-e2e-playwright.yml
@@ -0,0 +1,100 @@
+name: Playwright E2E Tests
+on:
+ workflow_call:
+ inputs:
+ debug:
+ description: SSH Debug
+ default: false
+ type: boolean
+env:
+ E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }}
+ E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }}
+ E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }}
+ E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }}
+ E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }}
+ E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }}
+ E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }}
+
+ E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}
+ RI_ENCRYPTION_KEY: ${{ secrets.RI_ENCRYPTION_KEY }}
+ RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}
+ RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}
+ TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }}
+ E2E_VOLUME_PATH: '/usr/src/app'
+
+jobs:
+ e2e-playwright-chromium-docker:
+ name: E2E Playwright Chromium Docker Build Tests
+ timeout-minutes: 60
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install dependencies for Playwright tests
+ uses: ./.github/actions/install-deps
+ with:
+ dir-path: './tests/playwright'
+
+ - name: Install Playwright Browsers
+ working-directory: ./tests/playwright
+ run: yarn playwright install --with-deps
+
+ - name: Download Docker Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: docker-builds
+ path: ./release
+
+ - name: Load built docker image from workspace
+ run: |
+ docker image load -i ./release/docker/docker-linux-alpine.amd64.tar
+
+ - name: Set up redis test environments
+ run: |
+ TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \
+ docker compose -p e2e-rte \
+ -f tests/e2e/rte.docker-compose.yml \
+ up --detach --force-recreate
+
+ - name: Set up RI docker image
+ run: |
+ E2E_RI_ENCRYPTION_KEY="$E2E_RI_ENCRYPTION_KEY" \
+ RI_SERVER_TLS_CERT="$RI_SERVER_TLS_CERT" \
+ RI_SERVER_TLS_KEY="$RI_SERVER_TLS_KEY" \
+ docker compose -p e2e-ri-docker \
+ -f tests/e2e/docker.web.docker-compose.yml \
+ up --detach --force-recreate
+ sleep 30
+
+ - name: Run Playwright tests
+ timeout-minutes: 80
+ working-directory: ./tests/playwright
+ if: ${{ !cancelled() }}
+ run: |
+ yarn test:chromium:docker
+
+ - uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() }}
+ with:
+ name: playwright-report
+ path: |
+ ./tests/playwright/test-results
+ ./tests/playwright/allure-results
+ ./tests/playwright/playwright-report
+ retention-days: 10
+
+ - name: Clean up redis test environments
+ if: always()
+ run: |
+ docker compose -p e2e-rte \
+ -f tests/e2e/rte.docker-compose.yml \
+ down --volumes --remove-orphans
+
+ - name: Clean up RI docker image
+ if: always()
+ run: |
+ docker compose -p e2e-ri-docker \
+ -f tests/e2e/docker.web.docker-compose.yml \
+ down --volumes --remove-orphans
diff --git a/.github/workflows/tests-e2e.yml b/.github/workflows/tests-e2e.yml
new file mode 100644
index 0000000000..00e293b03c
--- /dev/null
+++ b/.github/workflows/tests-e2e.yml
@@ -0,0 +1,83 @@
+name: ✅ E2E Tests
+
+on:
+ pull_request:
+ types: [labeled]
+
+ workflow_dispatch:
+ inputs:
+ debug:
+ description: Enable SSH Debug (IT and E2E)
+ default: false
+ type: boolean
+
+# Cancel a previous run workflow
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}-e2e
+ cancel-in-progress: true
+
+jobs:
+ # E2E Approve
+ e2e-approve:
+ runs-on: ubuntu-latest
+ if: github.event.action == 'labeled' && contains(github.event.label.name, 'e2e-approved') || github.event_name == 'workflow_dispatch'
+ name: Approve E2E tests
+ steps:
+ - name: Add "e2e-approved" Label
+ uses: actions-ecosystem/action-add-labels@v1
+ with:
+ labels: e2e-approved
+
+ # E2E Docker
+ build-docker:
+ uses: ./.github/workflows/pipeline-build-docker.yml
+ needs: e2e-approve
+ secrets: inherit
+ with:
+ debug: ${{ inputs.debug || false }}
+ for_e2e_tests: true
+
+ e2e-docker-tests:
+ needs: build-docker
+ uses: ./.github/workflows/tests-e2e-docker.yml
+ secrets: inherit
+ with:
+ debug: ${{ inputs.debug || false }}
+
+ tests-e2e-playwright:
+ needs: build-docker
+ uses: ./.github/workflows/tests-e2e-playwright.yml
+ secrets: inherit
+ with:
+ debug: ${{ inputs.debug || false }}
+
+ # E2E AppImage
+ build-appimage:
+ uses: ./.github/workflows/pipeline-build-linux.yml
+ needs: e2e-approve
+ secrets: inherit
+ with:
+ target: build_linux_appimage_x64
+ debug: ${{ inputs.debug || false }}
+
+ e2e-appimage-tests:
+ needs: build-appimage
+ uses: ./.github/workflows/tests-e2e-appimage.yml
+ secrets: inherit
+ with:
+ debug: ${{ inputs.debug || false }}
+
+ clean:
+ uses: ./.github/workflows/clean-deployments.yml
+ if: always()
+ needs: [e2e-docker-tests, e2e-appimage-tests, tests-e2e-playwright]
+
+ # Remove artifacts from github actions
+ remove-artifacts:
+ name: Remove artifacts
+ needs: [e2e-docker-tests, e2e-appimage-tests, tests-e2e-playwright]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Remove all artifacts
+ uses: ./.github/actions/remove-artifacts
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 7040d652c8..e65264f9ab 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -6,22 +6,12 @@ on:
- 'fe/**'
- 'be/**'
- 'fe-be/**'
- - 'e2e/**'
- 'feature/**'
- 'bugfix/**'
- 'ric/**'
workflow_dispatch:
inputs:
- group_tests:
- description: Run group of tests
- default: 'all'
- type: choice
- options:
- - all
- - without_e2e
- - only_e2e
-
redis_client:
description: Library to use for redis connection
default: 'ioredis'
@@ -42,10 +32,6 @@ on:
workflow_call:
inputs:
- group_tests:
- description: Run group of tests
- type: string
- default: 'without_e2e'
short_rte_list:
description: Use short rte list
type: boolean
@@ -71,7 +57,6 @@ jobs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
desktop: ${{ steps.filter.outputs.desktop }}
- e2e: ${{ steps.filter.outputs.e2e }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3.0.2
@@ -85,12 +70,10 @@ jobs:
- 'redisinsight/api/**'
desktop:
- 'redisinsight/desktop/**'
- e2e:
- - 'tests/e2e/**'
frontend-tests:
needs: changes
- if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'fe/') || startsWith(github.ref_name, 'fe-be/') || startsWith(github.ref_name, 'feature/') || startsWith(github.ref_name, 'bugfix/') || startsWith(github.ref_name, 'ric/')
+ if: startsWith(github.ref_name, 'fe/') || startsWith(github.ref_name, 'fe-be/') || startsWith(github.ref_name, 'feature/') || startsWith(github.ref_name, 'bugfix/') || startsWith(github.ref_name, 'ric/')
uses: ./.github/workflows/tests-frontend.yml
secrets: inherit
@@ -104,7 +87,7 @@ jobs:
backend-tests:
needs: changes
- if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'be/') || startsWith(github.ref_name, 'fe-be/') || startsWith(github.ref_name, 'feature/') || startsWith(github.ref_name, 'bugfix/') || startsWith(github.ref_name, 'ric/')
+ if: startsWith(github.ref_name, 'be/') || startsWith(github.ref_name, 'fe-be/') || startsWith(github.ref_name, 'feature/') || startsWith(github.ref_name, 'bugfix/') || startsWith(github.ref_name, 'ric/')
uses: ./.github/workflows/tests-backend.yml
secrets: inherit
@@ -118,7 +101,7 @@ jobs:
integration-tests:
needs: changes
- if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'be/') || startsWith(github.ref_name, 'fe-be/') || startsWith(github.ref_name, 'feature/') || startsWith(github.ref_name, 'bugfix/') || startsWith(github.ref_name, 'ric/')
+ if: startsWith(github.ref_name, 'be/') || startsWith(github.ref_name, 'fe-be/') || startsWith(github.ref_name, 'feature/') || startsWith(github.ref_name, 'bugfix/') || startsWith(github.ref_name, 'ric/')
uses: ./.github/workflows/tests-integration.yml
secrets: inherit
with:
@@ -134,49 +117,6 @@ jobs:
resource_name: integration-coverage
type: integration
- # # E2E Approve
- e2e-approve:
- runs-on: ubuntu-latest
- needs: changes
- if: inputs.group_tests == 'all' || inputs.group_tests == 'only_e2e' || startsWith(github.ref_name, 'e2e/')
- timeout-minutes: 60
- environment: ${{ startsWith(github.ref_name, 'e2e/') && 'e2e-approve' || 'staging' }}
- name: Approve E2E tests
- steps:
- - uses: actions/checkout@v4
-
- # E2E Docker
- build-docker:
- uses: ./.github/workflows/pipeline-build-docker.yml
- needs: e2e-approve
- secrets: inherit
- with:
- debug: ${{ inputs.debug || false }}
- for_e2e_tests: true
-
- e2e-docker-tests:
- needs: build-docker
- uses: ./.github/workflows/tests-e2e-docker.yml
- secrets: inherit
- with:
- debug: ${{ inputs.debug || false }}
-
- # E2E AppImage
- build-appimage:
- uses: ./.github/workflows/pipeline-build-linux.yml
- needs: e2e-approve
- secrets: inherit
- with:
- target: build_linux_appimage_x64
- debug: ${{ inputs.debug || false }}
-
- e2e-appimage-tests:
- needs: build-appimage
- uses: ./.github/workflows/tests-e2e-appimage.yml
- secrets: inherit
- with:
- debug: ${{ inputs.debug || false }}
-
clean:
uses: ./.github/workflows/clean-deployments.yml
if: always()
@@ -185,8 +125,6 @@ jobs:
frontend-tests,
backend-tests,
integration-tests,
- e2e-docker-tests,
- e2e-appimage-tests,
]
# Remove artifacts from github actions
@@ -197,8 +135,6 @@ jobs:
frontend-tests,
backend-tests,
integration-tests,
- e2e-docker-tests,
- e2e-appimage-tests,
]
runs-on: ubuntu-latest
steps:
diff --git a/package.json b/package.json
index db35cc1c65..112f1fa018 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"private": true,
"scripts": {
"dev:ui": "cross-env yarn --cwd redisinsight/ui dev",
+ "dev:ui:coverage": "cross-env COLLECT_COVERAGE=true yarn --cwd redisinsight/ui dev",
"dev:api": "cross-env yarn --cwd redisinsight/api start:dev",
"dev:electron:ui": "cross-env RI_APP_PORT=8080 RI_APP_TYPE=ELECTRON NODE_ENV=development yarn --cwd redisinsight/ui dev",
"dev:electron:api": "cross-env RI_APP_PORT=5540 RI_APP_TYPE=ELECTRON NODE_ENV=development USE_TCP_CLOUD_AUTH=true yarn --cwd redisinsight/api start:dev",
@@ -222,6 +223,7 @@
"vite-plugin-ejs": "^1.7.0",
"vite-plugin-electron": "^0.28.6",
"vite-plugin-electron-renderer": "^0.14.5",
+ "vite-plugin-istanbul": "^7.1.0",
"vite-plugin-react-click-to-component": "^3.0.0",
"vite-plugin-svgr": "^4.2.0",
"webpack": "^5.95.0",
diff --git a/redisinsight/ui/vite.config.mjs b/redisinsight/ui/vite.config.mjs
index 6047445739..81c5a3f6ac 100644
--- a/redisinsight/ui/vite.config.mjs
+++ b/redisinsight/ui/vite.config.mjs
@@ -5,6 +5,7 @@ import svgr from 'vite-plugin-svgr';
import fixReactVirtualized from 'esbuild-plugin-react-virtualized';
import { reactClickToComponent } from 'vite-plugin-react-click-to-component';
import { ViteEjsPlugin } from 'vite-plugin-ejs';
+import istanbul from 'vite-plugin-istanbul';
// import { compression } from 'vite-plugin-compression2'
import { fileURLToPath, URL } from 'url';
import path from 'path';
@@ -47,8 +48,26 @@ export default defineConfig({
})};`;
return html.replace(/
/, `\n ${script}`);
- }
- }
+ },
+ },
+ // Add istanbul plugin for coverage collection when COLLECT_COVERAGE is true
+ ...(process.env.COLLECT_COVERAGE === 'true'
+ ? [
+ istanbul({
+ include: 'src/**/*',
+ exclude: [
+ 'node_modules',
+ 'test/',
+ '**/*.spec.ts',
+ '**/*.spec.tsx',
+ '**/*.test.ts',
+ '**/*.test.tsx',
+ ],
+ extension: ['.js', '.ts', '.tsx'],
+ requireEnv: false,
+ }),
+ ]
+ : []),
// !isElectron && compression({
// include: [/\.(js)$/, /\.(css)$/],
// deleteOriginalAssets: true
diff --git a/tests/e2e/.env b/tests/e2e/.env
index 9242dec68e..21c248e7a3 100644
--- a/tests/e2e/.env
+++ b/tests/e2e/.env
@@ -6,3 +6,4 @@ RI_NOTIFICATION_UPDATE_URL=https://s3.amazonaws.com/redisinsight.test/public/tes
RI_NOTIFICATION_SYNC_INTERVAL=30000
RI_FEATURES_CONFIG_URL=http://static-server:5551/remote/features-config.json
RI_FEATURES_CONFIG_SYNC_INTERVAL=50000
+TEST_BIG_DB_DUMP=https://s3.amazonaws.com/redisinsight.test/public/rte/dump/big/dump.tar.gz
diff --git a/tests/e2e/docker.web.docker-compose.yml b/tests/e2e/docker.web.docker-compose.yml
index be7bdc84b3..1aa501ad44 100644
--- a/tests/e2e/docker.web.docker-compose.yml
+++ b/tests/e2e/docker.web.docker-compose.yml
@@ -2,6 +2,8 @@ version: "3.4"
services:
e2e:
+ profiles:
+ - e2e
build:
context: .
dockerfile: e2e.Dockerfile
@@ -37,6 +39,8 @@ services:
# Built image
app:
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
logging:
driver: none
image: redisinsight:amd64
@@ -51,6 +55,8 @@ services:
- rihomedir:/data
- tmp:/tmp
- ./test-data:/test-data
+ ports:
+ - 5540:5540
volumes:
tmp:
diff --git a/tests/e2e/local.web.docker-compose.yml b/tests/e2e/local.web.docker-compose.yml
index 788a88304e..b889d935d0 100644
--- a/tests/e2e/local.web.docker-compose.yml
+++ b/tests/e2e/local.web.docker-compose.yml
@@ -2,6 +2,8 @@ version: "3.4"
services:
e2e:
+ profiles:
+ - e2e
build:
context: .
dockerfile: e2e.Dockerfile
diff --git a/tests/playwright/.gitignore b/tests/playwright/.gitignore
new file mode 100644
index 0000000000..ee4b77c6b7
--- /dev/null
+++ b/tests/playwright/.gitignore
@@ -0,0 +1,10 @@
+
+# Playwright
+node_modules/
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+/allure-results/
+/.nyc_output/
+/coverage/
diff --git a/tests/playwright/.nycrc.json b/tests/playwright/.nycrc.json
new file mode 100644
index 0000000000..553d10ef43
--- /dev/null
+++ b/tests/playwright/.nycrc.json
@@ -0,0 +1,19 @@
+{
+ "all": true,
+ "cwd": "../../",
+ "include": ["redisinsight/ui/src/**/*.{ts,tsx,js,jsx}"],
+ "exclude": [
+ "redisinsight/ui/src/**/*.{test,spec}.{ts,tsx,js,jsx}",
+ "redisinsight/ui/src/**/__tests__/**",
+ "redisinsight/ui/src/**/__mocks__/**",
+ "redisinsight/ui/src/**/*.d.ts",
+ "redisinsight/ui/src/**/node_modules/**"
+ ],
+ "reporter": ["text", "html", "lcov"],
+ "report-dir": "tests/playwright/coverage",
+ "temp-dir": "tests/playwright/.nyc_output",
+ "cache": false,
+ "check-coverage": false,
+ "skip-full": false,
+ "skip-empty": false
+}
diff --git a/tests/playwright/README.md b/tests/playwright/README.md
new file mode 100644
index 0000000000..19897b5276
--- /dev/null
+++ b/tests/playwright/README.md
@@ -0,0 +1,202 @@
+# RedisInsight Playwright Tests
+
+This project contains end-to-end tests for RedisInsight using [Playwright](https://playwright.dev/). It supports running tests against three different RedisInsight builds:
+
+- **Docker Build**
+- **Electron Build**
+- **Local Web Build** (built directly from the source code)
+
+---
+
+## Installation
+
+> _Note: All commands below should be run from the `tests/playwright` directory._
+
+Before running any tests, make sure you have the dependencies installed:
+
+1. Install Node dependencies:
+
+ ```shell
+ yarn install
+ ```
+
+2. Install Playwright browsers:
+
+ ```shell
+ yarn playwright install
+ ```
+
+3. Install Playwright OS dependencies (Linux only):
+
+ ```shell
+ sudo yarn playwright install-deps
+ ```
+
+## Prerequisites
+
+- Docker installed and running.
+- Redis test environment and RedisInsight configurations from the `tests/e2e` project.
+
+## Environment-Specific Setup and Test Execution
+
+For more details, refer to the [Playwright documentation](https://playwright.dev/docs/running-tests).
+
+### Start Redis Test Environment (Required for All Builds)
+
+Navigate to the `tests/e2e` directory and run:
+
+```shell
+docker compose -p test-docker -f rte.docker-compose.yml up --force-recreate --detach
+```
+
+### Docker Build
+
+- Build the Docker image locally or trigger a [GitHub Action](https://github.com/RedisInsight/RedisInsight/actions/workflows/manual-build.yml) to build and download the artifact (`docker-linux-alpine.amd64.tar`).
+- Load the image:
+ ```shell
+ docker load -i docker-linux-alpine.amd64.tar
+ ```
+- Ensure the following environment variables are set in `tests/e2e/.env`:
+ - `RI_ENCRYPTION_KEY`
+ - `RI_SERVER_TLS_CERT`
+ - `RI_SERVER_TLS_KEY`
+- Navigate to the `tests/e2e` directory and start the container:
+ ```shell
+ docker compose -p e2e-ri-docker -f docker.web.docker-compose.yml up --detach --force-recreate
+ ```
+- Validate app is running at: `https://localhost:5540`.
+
+#### Run Playwright Tests
+
+_Note: Make sure to run the commands bellow from the `e2e/playwright` directory._
+
+Run all tests:
+
+```shell
+yarn test:chromium:docker
+```
+
+Run in debug mode:
+
+```shell
+yarn test:chromium:docker:debug
+```
+
+Run a specific spec file:
+
+```shell
+yarn test:chromium:docker basic-navigation
+```
+
+---
+
+### Electron Build
+
+- Build the project from the root directory:
+ ```shell
+ yarn package:prod
+ ```
+- Update `ELECTRON_EXECUTABLE_PATH` in `tests/playwright/env/.desktop.env` to point to the generated executable file (MacOS by default).
+
+#### Run Playwright Tests
+
+_Note: Make sure to run the commands bellow from the `e2e/playwright` directory._
+
+```shell
+yarn test:electron
+```
+
+---
+
+### Local Web Build
+
+- Make sure you don't have anything (docker container, local server, etc.) running on port 5540.
+- Start the UI and API servers:
+ ```shell
+ yarn dev:ui
+ yarn dev:api
+ ```
+- Access the app at: `http://localhost:8080`.
+
+#### Run Playwright Tests
+
+_Note: Make sure to run the command bellow from the `e2e/playwright` directory._
+
+```shell
+yarn test:chromium:local-web
+```
+
+## Folder structure
+
+- `/env` - contains env configs for the 3 types of builds.
+- `/tests` - Contains the actual tests.
+- `/helpers/api` - ported some api helpers from the tests/e2e project. They are used for setting up data.
+- `/pageObjects` - ported page element locators and logic from the tests/e2e project.
+
+## Extra Tooling
+
+### Auto-Generate Tests
+
+Use Playwright's Codegen to auto-generate tests:
+
+```shell
+yarn playwright codegen
+```
+
+### Interactive UI Mode
+
+Start Playwright's interactive UI mode:
+
+```shell
+yarn playwright test --ui
+```
+
+## Reports
+
+### Allure Reports
+
+- Ensure `JAVA_HOME` is set and JDK version 8 to 11 is installed.
+- Generate a report with history:
+ ```shell
+ yarn test:allureHistoryReport
+ ```
+- For more details, refer to the [Allure documentation](https://allurereport.org/docs/playwright-reference/).
+
+### Execution Time Comparison
+
+| Test Name | Framework | Browser | Duration |
+| --------------------------------- | ---------- | -------- | -------- |
+| Verify that user can add Hash Key | TestCafe | Chromium | 27s |
+| Verify that user can add Hash Key | Playwright | Chromium | 10s |
+| Verify that user can add Hash Key | TestCafe | Electron | 30s |
+| Verify that user can add Hash Key | Playwright | Electron | 18s |
+
+## Code Coverage
+
+### Overview
+
+The Playwright tests can collect code coverage for the React frontend application. This helps track which parts of the UI code are being exercised by the end-to-end tests.
+
+### Quick Start
+
+# Start the UI with instrumentation for collecting code coverage
+
+Ensure UI app is running with `COLLECT_COVERAGE=true` env variable, or simply run the following helper from the root folder
+
+```shell
+yarn dev:ui:coverage
+```
+
+# Run tests with coverage and generate both text and HTML reports
+
+```shell
+cd tests/playwright
+yarn test:coverage
+```
+
+### Coverage Reports Location
+
+After running coverage tests, reports are generated in:
+
+- **HTML Report**: `tests/playwright/coverage/index.html` - Interactive, browsable coverage report
+- **LCOV Report**: `tests/playwright/coverage/lcov.info` - For CI/CD integration
diff --git a/tests/playwright/env/.desktop.env b/tests/playwright/env/.desktop.env
new file mode 100644
index 0000000000..84513a228b
--- /dev/null
+++ b/tests/playwright/env/.desktop.env
@@ -0,0 +1,56 @@
+COMMON_URL=
+API_URL=http://localhost:5530/api
+ELECTRON_EXECUTABLE_PATH=../../release/mac-arm64/Redis Insight.app/Contents/MacOS/Redis Insight
+
+RI_APP_FOLDER_NAME=.redis-insight-stage
+
+OSS_STANDALONE_HOST=localhost
+OSS_STANDALONE_PORT=8100
+
+OSS_STANDALONE_V5_HOST=localhost
+OSS_STANDALONE_V5_PORT=8101
+
+OSS_STANDALONE_V7_HOST=localhost
+OSS_STANDALONE_V7_PORT=8108
+
+OSS_STANDALONE_V8_HOST=localhost
+OSS_STANDALONE_V8_PORT=8109
+
+OSS_STANDALONE_REDISEARCH_HOST=localhost
+OSS_STANDALONE_REDISEARCH_PORT=8102
+
+OSS_STANDALONE_BIG_HOST=localhost
+OSS_STANDALONE_BIG_PORT=8103
+
+OSS_STANDALONE_TLS_HOST=localhost
+OSS_STANDALONE_TLS_PORT=8104
+
+OSS_STANDALONE_EMPTY_HOST=localhost
+OSS_STANDALONE_EMPTY_PORT=8105
+
+OSS_STANDALONE_REDISGEARS_HOST=localhost
+OSS_STANDALONE_REDISGEARS_PORT=8106
+
+OSS_STANDALONE_NOPERM_HOST=localhost
+OSS_STANDALONE_NOPERM_PORT=8100
+
+OSS_CLUSTER_REDISGEARS_2_HOST=localhost
+OSS_CLUSTER_REDISGEARS_2_PORT=8107
+
+OSS_CLUSTER_HOST=localhost
+OSS_CLUSTER_PORT=8200
+
+OSS_SENTINEL_HOST=localhost
+OSS_SENTINEL_PORT=28100
+OSS_SENTINEL_PASSWORD=password
+
+RE_CLUSTER_HOST=localhost
+RE_CLUSTER_PORT=19443
+
+RI_NOTIFICATION_UPDATE_URL=https://s3.amazonaws.com/redisinsight.test/public/tests/e2e/notifications.json
+RI_NOTIFICATION_SYNC_INTERVAL=30000
+
+RI_FEATURES_CONFIG_URL=http://localhost:5551/remote/features-config.json
+RI_FEATURES_CONFIG_SYNC_INTERVAL=50000
+
+REMOTE_FOLDER_PATH=/home/runner/work/RedisInsight/RedisInsight/tests/e2e/remote
diff --git a/tests/playwright/env/.docker.env b/tests/playwright/env/.docker.env
new file mode 100644
index 0000000000..4cdc9c6d5a
--- /dev/null
+++ b/tests/playwright/env/.docker.env
@@ -0,0 +1,47 @@
+COMMON_URL=https://localhost:5540
+API_URL=https://localhost:5540/api
+
+RI_APP_FOLDER_NAME=.redis-insight
+
+OSS_STANDALONE_HOST=host.docker.internal
+OSS_STANDALONE_PORT=8100
+
+OSS_STANDALONE_V5_HOST=host.docker.internal
+OSS_STANDALONE_V5_PORT=8101
+
+OSS_STANDALONE_V7_HOST=host.docker.internal
+OSS_STANDALONE_V7_PORT=8108
+
+OSS_STANDALONE_V8_HOST=host.docker.internal
+OSS_STANDALONE_V8_PORT=8109
+
+OSS_STANDALONE_REDISEARCH_HOST=host.docker.internal
+OSS_STANDALONE_REDISEARCH_PORT=8102
+
+OSS_STANDALONE_BIG_HOST=host.docker.internal
+OSS_STANDALONE_BIG_PORT=8103
+
+OSS_STANDALONE_TLS_HOST=host.docker.internal
+OSS_STANDALONE_TLS_PORT=8104
+
+OSS_STANDALONE_EMPTY_HOST=host.docker.internal
+OSS_STANDALONE_EMPTY_PORT=8105
+
+OSS_STANDALONE_REDISGEARS_HOST=host.docker.internal
+OSS_STANDALONE_REDISGEARS_PORT=8106
+
+OSS_STANDALONE_NOPERM_HOST=host.docker.internal
+OSS_STANDALONE_NOPERM_PORT=8100
+
+OSS_CLUSTER_REDISGEARS_2_HOST=host.docker.internal
+OSS_CLUSTER_REDISGEARS_2_PORT=8107
+
+OSS_CLUSTER_HOST=host.docker.internal
+OSS_CLUSTER_PORT=8200
+
+OSS_SENTINEL_HOST=host.docker.internal
+OSS_SENTINEL_PORT=28100
+OSS_SENTINEL_PASSWORD=password
+
+RE_CLUSTER_HOST=host.docker.internal
+RE_CLUSTER_PORT=19443
diff --git a/tests/playwright/env/.local-web.env b/tests/playwright/env/.local-web.env
new file mode 100644
index 0000000000..d7baae78ee
--- /dev/null
+++ b/tests/playwright/env/.local-web.env
@@ -0,0 +1,47 @@
+COMMON_URL=http://localhost:8080
+API_URL=http://localhost:5540/api
+
+RI_APP_FOLDER_NAME=.redis-insight
+
+OSS_STANDALONE_HOST=localhost
+OSS_STANDALONE_PORT=8100
+
+OSS_STANDALONE_V5_HOST=localhost
+OSS_STANDALONE_V5_PORT=8101
+
+OSS_STANDALONE_V7_HOST=localhost
+OSS_STANDALONE_V7_PORT=8108
+
+OSS_STANDALONE_V8_HOST=localhost
+OSS_STANDALONE_V8_PORT=8109
+
+OSS_STANDALONE_REDISEARCH_HOST=localhost
+OSS_STANDALONE_REDISEARCH_PORT=8102
+
+OSS_STANDALONE_BIG_HOST=localhost
+OSS_STANDALONE_BIG_PORT=8103
+
+OSS_STANDALONE_TLS_HOST=localhost
+OSS_STANDALONE_TLS_PORT=8104
+
+OSS_STANDALONE_EMPTY_HOST=localhost
+OSS_STANDALONE_EMPTY_PORT=8105
+
+OSS_STANDALONE_REDISGEARS_HOST=localhost
+OSS_STANDALONE_REDISGEARS_PORT=8106
+
+OSS_STANDALONE_NOPERM_HOST=localhost
+OSS_STANDALONE_NOPERM_PORT=8100
+
+OSS_CLUSTER_REDISGEARS_2_HOST=localhost
+OSS_CLUSTER_REDISGEARS_2_PORT=8107
+
+OSS_CLUSTER_HOST=localhost
+OSS_CLUSTER_PORT=8200
+
+OSS_SENTINEL_HOST=localhost
+OSS_SENTINEL_PORT=28100
+OSS_SENTINEL_PASSWORD=password
+
+RE_CLUSTER_HOST=localhost
+RE_CLUSTER_PORT=19443
diff --git a/tests/playwright/fixtures/test.ts b/tests/playwright/fixtures/test.ts
new file mode 100644
index 0000000000..f604033b7b
--- /dev/null
+++ b/tests/playwright/fixtures/test.ts
@@ -0,0 +1,168 @@
+/* eslint-disable no-empty-pattern */
+import { test as base, expect } from '@playwright/test'
+import {
+ BrowserContext,
+ ElectronApplication,
+ Page,
+ _electron as electron,
+} from 'playwright'
+import log from 'node-color-log'
+import { AxiosInstance } from 'axios'
+import * as crypto from 'crypto'
+import fs from 'fs'
+import path from 'path'
+
+import { apiUrl, isElectron, electronExecutablePath } from '../helpers/conf'
+import { generateApiClient } from '../helpers/api/http-client'
+import { APIKeyRequests } from '../helpers/api/api-keys'
+import { DatabaseAPIRequests } from '../helpers/api/api-databases'
+import { UserAgreementDialog } from '../pageObjects'
+
+// Coverage type declaration
+declare global {
+ interface Window {
+ // eslint-disable-next-line no-underscore-dangle
+ __coverage__: any
+ }
+}
+
+export function generateUUID(): string {
+ return crypto.randomBytes(16).toString('hex')
+}
+
+type CommonFixtures = {
+ forEachTest: void
+ api: {
+ apiClient: AxiosInstance
+ keyService: APIKeyRequests
+ databaseService: DatabaseAPIRequests
+ }
+}
+
+const commonTest = base.extend({
+ // Simple context setup for coverage
+ context: async ({ context }, use) => {
+ if (process.env.COLLECT_COVERAGE === 'true') {
+ const outputDir = path.join(process.cwd(), '.nyc_output')
+ await fs.promises.mkdir(outputDir, { recursive: true })
+
+ // Expose coverage collection function
+ await context.exposeFunction(
+ 'collectIstanbulCoverage',
+ (coverageJSON: string) => {
+ if (coverageJSON) {
+ fs.writeFileSync(
+ path.join(
+ outputDir,
+ `playwright_coverage_${generateUUID()}.json`,
+ ),
+ coverageJSON,
+ )
+ }
+ },
+ )
+ }
+
+ await use(context)
+ },
+
+ api: async ({ page }, use) => {
+ const windowId = await page.evaluate(() => window.windowId)
+
+ const apiClient = generateApiClient(apiUrl, windowId)
+ const databaseService = new DatabaseAPIRequests(apiClient)
+ const keyService = new APIKeyRequests(apiClient, databaseService)
+
+ await use({ apiClient, keyService, databaseService })
+ },
+ forEachTest: [
+ async ({ page }, use) => {
+ // before each test:
+ if (!isElectron) {
+ await page.goto('/')
+ } else {
+ await page.locator('[data-testid="home-tab-databases"]').click()
+ }
+
+ const userAgreementDialog = new UserAgreementDialog(page)
+ await userAgreementDialog.acceptLicenseTerms()
+
+ const skipTourElement = page.locator('button', {
+ hasText: 'Skip tour',
+ })
+ if (await skipTourElement.isVisible()) {
+ skipTourElement.click()
+ }
+
+ await use()
+
+ // Collect coverage after each test
+ if (process.env.COLLECT_COVERAGE === 'true') {
+ await page
+ .evaluate(() => {
+ if (
+ typeof window !== 'undefined' &&
+ // eslint-disable-next-line no-underscore-dangle
+ window.__coverage__
+ ) {
+ ;(window as any).collectIstanbulCoverage(
+ // eslint-disable-next-line no-underscore-dangle
+ JSON.stringify(window.__coverage__),
+ )
+ }
+ })
+ .catch(() => {
+ // Ignore errors - page might be closed
+ })
+ }
+ },
+ { auto: true },
+ ],
+})
+
+const electronTest = commonTest.extend<{
+ electronApp: ElectronApplication | null
+ page: Page
+ context: BrowserContext
+}>({
+ electronApp: async ({}, use) => {
+ const electronApp = await electron.launch({
+ executablePath: electronExecutablePath,
+ args: ['index.html'],
+ timeout: 60000,
+ })
+ electronApp.on('console', (msg) => {
+ log.info(`Electron Log: ${msg.type()} - ${msg.text()}`)
+ })
+
+ // Wait for window startup
+ await new Promise((resolve) => setTimeout(resolve, 2000))
+
+ await use(electronApp)
+
+ log.info('Closing Electron app...')
+ await electronApp.close()
+ },
+ page: async ({ electronApp }, use) => {
+ if (!electronApp) {
+ throw new Error('Electron app is not initialized')
+ }
+
+ const electronPage = await electronApp.firstWindow()
+
+ await use(electronPage)
+ },
+ context: async ({ electronApp }, use) => {
+ if (!electronApp) {
+ throw new Error('Electron app is not initialized')
+ }
+
+ const electronContext = electronApp.context()
+
+ await use(electronContext)
+ },
+})
+
+const test = isElectron ? electronTest : commonTest
+
+export { test, expect, isElectron }
diff --git a/tests/playwright/helpers/api/api-databases.ts b/tests/playwright/helpers/api/api-databases.ts
new file mode 100644
index 0000000000..af233a4dce
--- /dev/null
+++ b/tests/playwright/helpers/api/api-databases.ts
@@ -0,0 +1,98 @@
+import { faker } from '@faker-js/faker'
+import { AxiosInstance } from 'axios'
+import { AddNewDatabaseParameters, DatabaseInstance } from '../../types'
+import { ResourcePath } from '../constants'
+
+export class DatabaseAPIRequests {
+ constructor(private apiClient: AxiosInstance) {}
+
+ async addNewStandaloneDatabaseApi(
+ databaseParameters: AddNewDatabaseParameters,
+ isCloud = false,
+ ): Promise {
+ const uniqueId = faker.string.alphanumeric({ length: 10 })
+ const uniqueIdNumber = faker.number.int({ min: 1, max: 1000 })
+ const requestBody: any = {
+ name: databaseParameters.databaseName,
+ host: databaseParameters.host,
+ port: Number(databaseParameters.port),
+ }
+
+ if (databaseParameters.databaseUsername) {
+ requestBody.username = databaseParameters.databaseUsername
+ }
+
+ if (databaseParameters.databasePassword) {
+ requestBody.password = databaseParameters.databasePassword
+ }
+
+ if (databaseParameters.caCert) {
+ requestBody.tls = true
+ requestBody.verifyServerCert = false
+ requestBody.caCert = {
+ name: `ca-${uniqueId}`,
+ certificate: databaseParameters.caCert.certificate,
+ }
+ requestBody.clientCert = {
+ name: `client-${uniqueId}`,
+ certificate: databaseParameters.clientCert!.certificate,
+ key: databaseParameters.clientCert!.key,
+ }
+ }
+
+ if (isCloud) {
+ requestBody.cloudDetails = {
+ cloudId: uniqueIdNumber,
+ subscriptionType: 'fixed',
+ planMemoryLimit: 30,
+ memoryLimitMeasurementUnit: 'mb',
+ free: true,
+ }
+ }
+
+ const response = await this.apiClient.post(
+ ResourcePath.Databases,
+ requestBody,
+ )
+ if (response.status !== 201)
+ throw new Error(
+ `Database creation failed for ${databaseParameters.databaseName}`,
+ )
+ }
+
+ async getAllDatabases(): Promise {
+ const response = await this.apiClient.get(ResourcePath.Databases)
+ if (response.status !== 200)
+ throw new Error('Failed to retrieve databases')
+ return response.data
+ }
+
+ async getDatabaseIdByName(databaseName?: string): Promise {
+ if (!databaseName) throw new Error('Error: Missing databaseName')
+
+ const allDatabases = await this.getAllDatabases()
+ const foundDb = allDatabases.find((item) => item.name === databaseName)
+
+ if (!foundDb) throw new Error(`Database ${databaseName} not found`)
+
+ return foundDb.id
+ }
+
+ async deleteStandaloneDatabaseApi(
+ databaseParameters: AddNewDatabaseParameters,
+ ): Promise {
+ const databaseId = await this.getDatabaseIdByName(
+ databaseParameters.databaseName,
+ )
+ if (!databaseId) throw new Error('Error: Missing databaseId')
+
+ const requestBody = { ids: [databaseId] }
+ const response = await this.apiClient.delete(ResourcePath.Databases, {
+ data: requestBody,
+ })
+ if (response.status !== 200)
+ throw new Error(
+ `Failed to delete database ${databaseParameters.databaseName}`,
+ )
+ }
+}
diff --git a/tests/playwright/helpers/api/api-keys.ts b/tests/playwright/helpers/api/api-keys.ts
new file mode 100755
index 0000000000..b1d1809fdd
--- /dev/null
+++ b/tests/playwright/helpers/api/api-keys.ts
@@ -0,0 +1,130 @@
+/* eslint-disable max-len */
+import { AxiosInstance } from 'axios'
+import { DatabaseAPIRequests } from './api-databases'
+import {
+ AddNewDatabaseParameters,
+ HashKeyParameters,
+ SetKeyParameters,
+ StreamKeyParameters,
+} from '../../types'
+
+const bufferPathMask = '/databases/databaseId/keys?encoding=buffer'
+export class APIKeyRequests {
+ constructor(
+ private apiClient: AxiosInstance,
+ private databaseAPIRequests: DatabaseAPIRequests,
+ ) {}
+
+ async addHashKeyApi(
+ keyParameters: HashKeyParameters,
+ databaseParameters: AddNewDatabaseParameters,
+ ): Promise {
+ const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(
+ databaseParameters.databaseName,
+ )
+ const requestBody = {
+ keyName: Buffer.from(keyParameters.keyName, 'utf-8'),
+ fields: keyParameters.fields.map((fields) => ({
+ ...fields,
+ field: Buffer.from(fields.field, 'utf-8'),
+ value: Buffer.from(fields.value, 'utf-8'),
+ })),
+ }
+ const response = await this.apiClient.post(
+ `/databases/${databaseId}/hash?encoding=buffer`,
+ requestBody,
+ )
+ if (response.status !== 201)
+ throw new Error('The creation of new Hash key request failed')
+ }
+
+ async addStreamKeyApi(
+ keyParameters: StreamKeyParameters,
+ databaseParameters: AddNewDatabaseParameters,
+ ): Promise {
+ const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(
+ databaseParameters.databaseName,
+ )
+ const requestBody = {
+ keyName: Buffer.from(keyParameters.keyName, 'utf-8'),
+ entries: keyParameters.entries.map((member) => ({
+ ...member,
+ fields: member.fields.map(({ name, value }) => ({
+ name: Buffer.from(name, 'utf-8'),
+ value: Buffer.from(value, 'utf-8'),
+ })),
+ })),
+ }
+ const response = await this.apiClient.post(
+ `/databases/${databaseId}/streams?encoding=buffer`,
+ requestBody,
+ )
+ if (response.status !== 201)
+ throw new Error('The creation of new Stream key request failed')
+ }
+
+ async addSetKeyApi(
+ keyParameters: SetKeyParameters,
+ databaseParameters: AddNewDatabaseParameters,
+ ): Promise {
+ const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(
+ databaseParameters.databaseName,
+ )
+ const requestBody = {
+ keyName: Buffer.from(keyParameters.keyName, 'utf-8'),
+ members: keyParameters.members.map((member) =>
+ Buffer.from(member, 'utf-8'),
+ ),
+ }
+ const response = await this.apiClient.post(
+ `/databases/${databaseId}/set?encoding=buffer`,
+ requestBody,
+ )
+ if (response.status !== 201)
+ throw new Error('The creation of new Set key request failed')
+ }
+
+ async searchKeyByNameApi(
+ keyName: string,
+ databaseName: string,
+ ): Promise {
+ const requestBody = {
+ cursor: '0',
+ match: keyName,
+ }
+ const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(
+ databaseName,
+ )
+ const response = await this.apiClient.post(
+ bufferPathMask.replace('databaseId', databaseId),
+ requestBody,
+ )
+ if (response.status !== 200)
+ throw new Error('Getting key request failed')
+ return response.data[0].keys
+ }
+
+ async deleteKeyByNameApi(
+ keyName: string,
+ databaseName: string,
+ ): Promise {
+ const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(
+ databaseName,
+ )
+ const doesKeyExist = await this.searchKeyByNameApi(
+ keyName,
+ databaseName,
+ )
+ if (doesKeyExist.length > 0) {
+ const requestBody = { keyNames: [Buffer.from(keyName, 'utf-8')] }
+ const response = await this.apiClient.delete(
+ bufferPathMask.replace('databaseId', databaseId),
+ {
+ data: requestBody,
+ },
+ )
+ if (response.status !== 200)
+ throw new Error('The deletion of the key request failed')
+ }
+ }
+}
diff --git a/tests/playwright/helpers/api/http-client.ts b/tests/playwright/helpers/api/http-client.ts
new file mode 100644
index 0000000000..24d352e454
--- /dev/null
+++ b/tests/playwright/helpers/api/http-client.ts
@@ -0,0 +1,34 @@
+import axios, { AxiosInstance } from 'axios'
+import https from 'https'
+
+export function generateApiClient(apiUrl: string, windowId?: string): AxiosInstance {
+ const apiClient = axios.create({
+ baseURL: apiUrl,
+ headers: {
+ 'X-Window-Id': windowId,
+ },
+ httpsAgent: new https.Agent({
+ rejectUnauthorized: false, // Allows self-signed/invalid SSL certs
+ }),
+ })
+
+ // Enable logging if DEBUG is set
+ if (process.env.DEBUG) {
+ this.apiClient.interceptors.request.use((request) => {
+ console.log('Starting Request', request)
+ return request
+ })
+ this.apiClient.interceptors.response.use(
+ (response) => {
+ console.log('Response:', response)
+ return response
+ },
+ (error) => {
+ console.error('Error Response:', error.response)
+ return Promise.reject(error)
+ },
+ )
+ }
+
+ return apiClient
+}
diff --git a/tests/playwright/helpers/conf.ts b/tests/playwright/helpers/conf.ts
new file mode 100644
index 0000000000..306d687069
--- /dev/null
+++ b/tests/playwright/helpers/conf.ts
@@ -0,0 +1,209 @@
+import { faker } from '@faker-js/faker'
+import * as os from 'os'
+import * as fs from 'fs'
+import { join as joinPath } from 'path'
+import * as path from 'path'
+
+// Urls for using in the tests
+export const commonUrl = process.env.COMMON_URL || 'https://localhost:5540'
+export const apiUrl = process.env.API_URL || 'https://localhost:5540/api'
+export const electronExecutablePath = process.env.ELECTRON_EXECUTABLE_PATH
+export const isElectron = electronExecutablePath !== undefined
+export const googleUser = process.env.GOOGLE_USER || ''
+export const googleUserPassword = process.env.GOOGLE_USER_PASSWORD || ''
+export const samlUser = process.env.E2E_SSO_EMAIL || ''
+export const samlUserPassword = process.env.E2E_SSO_PASSWORD || ''
+
+export const workingDirectory =
+ process.env.RI_APP_FOLDER_ABSOLUTE_PATH ||
+ joinPath(os.homedir(), process.env.RI_APP_FOLDER_NAME || '.redis-insight')
+export const fileDownloadPath = joinPath(os.homedir(), 'Downloads')
+const uniqueId = faker.string.alphanumeric({ length: 10 })
+
+export const ossStandaloneConfig = {
+ host: process.env.OSS_STANDALONE_HOST!,
+ port: process.env.OSS_STANDALONE_PORT!,
+ databaseName: `${process.env.OSS_STANDALONE_DATABASE_NAME || 'test_standalone'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_PASSWORD,
+}
+
+export const ossStandaloneConfigEmpty = {
+ host: process.env.OSS_STANDALONE_EMPTY_HOST,
+ port: process.env.OSS_STANDALONE_EMPTY_PORT,
+ databaseName: `${process.env.OSS_STANDALONE_EMPTY_DATABASE_NAME || 'test_standalone_empty'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_EMPTY_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_EMPTY_PASSWORD,
+}
+
+export const ossStandaloneV5Config = {
+ host: process.env.OSS_STANDALONE_V5_HOST,
+ port: process.env.OSS_STANDALONE_V5_PORT,
+ databaseName: `${process.env.OSS_STANDALONE_V5_DATABASE_NAME || 'test_standalone-v5'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_V5_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_V5_PASSWORD,
+}
+
+export const ossStandaloneV7Config = {
+ host: process.env.OSS_STANDALONE_V7_HOST,
+ port: process.env.OSS_STANDALONE_V7_PORT,
+ databaseName: `${process.env.OSS_STANDALONE_V7_DATABASE_NAME || 'test_standalone-v7'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_V7_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_V7_PASSWORD,
+}
+
+export const ossStandaloneV6Config = {
+ host: process.env.OSS_STANDALONE_V8_HOST,
+ port: process.env.OSS_STANDALONE_V8_PORT,
+ databaseName: `${process.env.OSS_STANDALONE_V8_DATABASE_NAME || 'test_standalone-v6'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_V8_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_V8_PASSWORD,
+}
+
+export const ossStandaloneRedisearch = {
+ host: process.env.OSS_STANDALONE_REDISEARCH_HOST,
+ port: process.env.OSS_STANDALONE_REDISEARCH_PORT,
+ databaseName: `${process.env.OSS_STANDALONE_REDISEARCH_DATABASE_NAME || 'test_standalone-redisearch'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_REDISEARCH_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_REDISEARCH_PASSWORD,
+}
+
+export const ossClusterConfig = {
+ ossClusterHost: process.env.OSS_CLUSTER_HOST,
+ ossClusterPort: process.env.OSS_CLUSTER_PORT,
+ ossClusterDatabaseName: `${process.env.OSS_CLUSTER_DATABASE_NAME || 'test_cluster'}-${uniqueId}`,
+}
+
+export const ossSentinelConfig = {
+ sentinelHost: process.env.OSS_SENTINEL_HOST,
+ sentinelPort: process.env.OSS_SENTINEL_PORT,
+ sentinelPassword: process.env.OSS_SENTINEL_PASSWORD,
+ masters: [
+ {
+ alias: `primary-group-1}-${uniqueId}`,
+ db: '0',
+ name: 'primary-group-1',
+ password: 'defaultpass',
+ },
+ {
+ alias: `primary-group-2}-${uniqueId}`,
+ db: '0',
+ name: 'primary-group-2',
+ password: 'defaultpass',
+ },
+ ],
+ name: ['primary-group-1', 'primary-group-2'],
+}
+
+export const redisEnterpriseClusterConfig = {
+ host: process.env.RE_CLUSTER_HOST,
+ port: process.env.RE_CLUSTER_PORT,
+ databaseName: process.env.RE_CLUSTER_DATABASE_NAME || 'test-re-standalone',
+ databaseUsername: process.env.RE_CLUSTER_ADMIN_USER || 'demo@redislabs.com',
+ databasePassword: process.env.RE_CLUSTER_ADMIN_PASSWORD || '123456',
+}
+
+export const invalidOssStandaloneConfig = {
+ host: 'oss-standalone-invalid',
+ port: '1010',
+ databaseName: `${process.env.OSS_STANDALONE_INVALID_DATABASE_NAME || 'test_standalone-invalid'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_INVALID_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_INVALID_PASSWORD,
+}
+
+export const ossStandaloneBigConfig = {
+ host: process.env.OSS_STANDALONE_BIG_HOST,
+ port: process.env.OSS_STANDALONE_BIG_PORT,
+ databaseName: `${process.env.OSS_STANDALONE_BIG_DATABASE_NAME || 'test_standalone_big'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_BIG_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_BIG_PASSWORD,
+}
+
+export const cloudDatabaseConfig = {
+ host: process.env.E2E_CLOUD_DATABASE_HOST || '',
+ port: process.env.E2E_CLOUD_DATABASE_PORT || '',
+ databaseName: `${process.env.E2E_CLOUD_DATABASE_NAME || 'cloud-database'}-${uniqueId}`,
+ databaseUsername: process.env.E2E_CLOUD_DATABASE_USERNAME,
+ databasePassword: process.env.E2E_CLOUD_DATABASE_PASSWORD,
+ accessKey: process.env.E2E_CLOUD_API_ACCESS_KEY || '',
+ secretKey: process.env.E2E_CLOUD_API_SECRET_KEY || '',
+}
+
+export const ossStandaloneNoPermissionsConfig = {
+ host: process.env.OSS_STANDALONE_NOPERM_HOST,
+ port: process.env.OSS_STANDALONE_NOPERM_PORT,
+ databaseName: `${process.env.OSS_STANDALONE_NOPERM_DATABASE_NAME || 'oss-standalone-no-permissions'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_NOPERM_USERNAME || 'noperm',
+ databasePassword: process.env.OSS_STANDALONE_NOPERM_PASSWORD,
+}
+
+export const ossStandaloneForSSHConfig = {
+ host: process.env.OSS_STANDALONE_SSH_HOST || '172.33.100.111',
+ port: process.env.OSS_STANDALONE_SSH_PORT || '6379',
+ databaseName: `${process.env.OSS_STANDALONE_SSH_DATABASE_NAME || 'oss-standalone-for-ssh'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_SSH_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_SSH_PASSWORD,
+}
+
+export const ossClusterForSSHConfig = {
+ host: process.env.OSS_CLUSTER_SSH_HOST || '172.31.100.211',
+ port: process.env.OSS_CLUSTER_SSH_PORT || '6379',
+ databaseName: `${process.env.OSS_CLUSTER_SSH_DATABASE_NAME || 'oss-cluster-for-ssh'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_CLUSTER_SSH_USERNAME,
+ databasePassword: process.env.OSS_CLUSTER_SSH_PASSWORD,
+}
+
+export const ossStandaloneTlsConfig = {
+ host: process.env.OSS_STANDALONE_TLS_HOST,
+ port: process.env.OSS_STANDALONE_TLS_PORT,
+ databaseName: `${process.env.OSS_STANDALONE_TLS_DATABASE_NAME || 'test_standalone_tls'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_TLS_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_TLS_PASSWORD,
+ caCert: {
+ name: `ca}-${uniqueId}`,
+ certificate:
+ process.env.E2E_CA_CRT ||
+ fs.readFileSync(
+ path.resolve(
+ __dirname,
+ '../../e2e/rte/oss-standalone-tls/certs/redisCA.crt',
+ ),
+ 'utf-8',
+ ),
+ },
+ clientCert: {
+ name: `client}-${uniqueId}`,
+ certificate:
+ process.env.E2E_CLIENT_CRT ||
+ fs.readFileSync(
+ path.resolve(
+ __dirname,
+ '../../e2e/rte/oss-standalone-tls/certs/redis.crt',
+ ),
+ 'utf-8',
+ ),
+ key:
+ process.env.E2E_CLIENT_KEY ||
+ fs.readFileSync(
+ path.resolve(
+ __dirname,
+ '../../e2e/rte/oss-standalone-tls/certs/redis.key',
+ ),
+ 'utf-8',
+ ),
+ },
+}
+
+export const ossStandaloneRedisGears = {
+ host: process.env.OSS_STANDALONE_REDISGEARS_HOST,
+ port: process.env.OSS_STANDALONE_REDISGEARS_PORT,
+ databaseName: `${process.env.OSS_STANDALONE_REDISGEARS_DATABASE_NAME || 'test_standalone_redisgears'}-${uniqueId}`,
+ databaseUsername: process.env.OSS_STANDALONE_REDISGEARS_USERNAME,
+ databasePassword: process.env.OSS_STANDALONE_REDISGEARS_PASSWORD,
+}
+
+export const ossClusterRedisGears = {
+ ossClusterHost: process.env.OSS_CLUSTER_REDISGEARS_2_HOST,
+ ossClusterPort: process.env.OSS_CLUSTER_REDISGEARS_2_PORT,
+ ossClusterDatabaseName: `${process.env.OSS_CLUSTER_REDISGEARS_2_NAME || 'test_cluster-gears-2.0'}-${uniqueId}`,
+}
diff --git a/tests/playwright/helpers/constants.ts b/tests/playwright/helpers/constants.ts
new file mode 100644
index 0000000000..af4dc6199d
--- /dev/null
+++ b/tests/playwright/helpers/constants.ts
@@ -0,0 +1,133 @@
+export enum KeyTypesTexts {
+ Hash = 'Hash',
+ List = 'List',
+ Set = 'Set',
+ ZSet = 'Sorted Set',
+ String = 'String',
+ ReJSON = 'JSON',
+ Stream = 'Stream',
+ Graph = 'Graph',
+ TimeSeries = 'Time Series',
+}
+export const keyLength = 50
+
+export const COMMANDS_TO_CREATE_KEY = Object.freeze({
+ [KeyTypesTexts.Hash]: (key: string, value: string | number = 'value', field: string | number = 'field') => `HSET ${key} '${field}' '${value}'`,
+ [KeyTypesTexts.List]: (key: string, element: string | number = 'element') => `LPUSH ${key} '${element}'`,
+ [KeyTypesTexts.Set]: (key: string, member = 'member') => `SADD ${key} '${member}'`,
+ [KeyTypesTexts.ZSet]: (key: string, member = 'member', score = 1) => `ZADD ${key} ${score} '${member}'`,
+ [KeyTypesTexts.String]: (key: string, value = 'val') => `SET ${key} '${value}'`,
+ [KeyTypesTexts.ReJSON]: (key: string, json = '"val"') => `JSON.SET ${key} . '${json}'`,
+ [KeyTypesTexts.Stream]: (key: string, value: string | number = 'value', field: string | number = 'field') => `XADD ${key} * '${field}' '${value}'`,
+ [KeyTypesTexts.Graph]: (key: string) => `GRAPH.QUERY ${key} "CREATE ()"`,
+ [KeyTypesTexts.TimeSeries]: (key: string) => `TS.CREATE ${key}`
+})
+
+export enum RTE {
+ none = 'none',
+ standalone = 'standalone',
+ sentinel = 'sentinel',
+ ossCluster = 'oss-cluster',
+ reCluster = 're-cluster',
+ reCloud = 're-cloud'
+}
+
+export enum ENV {
+ web = 'web',
+ desktop = 'desktop'
+}
+
+export enum RecommendationIds {
+ redisVersion = 'redisVersion',
+ searchVisualization = 'searchVisualization',
+ setPassword = 'setPassword',
+ optimizeTimeSeries = 'RTS',
+ luaScript = 'luaScript',
+ useSmallerKeys = 'useSmallerKeys',
+ avoidLogicalDatabases = 'avoidLogicalDatabases',
+ searchJson = 'searchJSON',
+ rdi = 'tryRDI'
+}
+
+export enum LibrariesSections {
+ Functions = 'Functions',
+ KeyspaceTriggers = 'Keyspace',
+ ClusterFunctions = 'Cluster',
+ StreamFunctions= 'Stream',
+}
+
+export enum FunctionsSections {
+ General = 'General',
+ Flag = 'Flag',
+}
+
+export enum MonacoEditorInputs {
+ // add library fields
+ Code = 'code-value',
+ Configuration = 'configuration-value',
+ // added library fields
+ Library = 'library-code',
+ LibraryConfiguration = 'library-configuration',
+}
+
+export enum ResourcePath {
+ Databases = '/databases',
+ RedisSentinel = '/redis-sentinel',
+ ClusterDetails = '/cluster-details',
+ SyncFeatures = '/features/sync',
+ Rdi = '/rdi'
+}
+
+export enum ExploreTabs {
+ Tutorials = 'Tutorials',
+ Tips = 'Tips',
+}
+
+export enum Compatibility {
+ SearchAndQuery = 'search',
+ Json = 'json',
+ TimeSeries = 'time-series'
+}
+
+export enum ChatBotTabs {
+ General = 'General',
+ Database = 'Database',
+}
+
+export enum RedisOverviewPage {
+ DataBase = 'Redis Databases',
+ Rdi = 'My RDI instances',
+}
+
+export enum TextConnectionSection {
+ Success = 'success',
+ Failed = 'failed',
+}
+
+export enum RdiTemplatePipelineType {
+ Ingest = 'ingest',
+ WriteBehind = 'write-behind',
+}
+
+export enum RdiTemplateDatabaseType {
+ SqlServer = 'sql',
+ Oracle = 'oracle',
+ MySql = 'mysql',
+}
+
+export enum RdiPopoverOptions {
+ Server = 'server',
+ File = 'file',
+ Pipeline = 'empty',
+}
+
+export enum TlsCertificates {
+ CA = 'ca',
+ Client = 'client',
+}
+
+export enum AddElementInList {
+ Head ,
+ Tail,
+}
+
diff --git a/tests/playwright/helpers/utils.ts b/tests/playwright/helpers/utils.ts
new file mode 100644
index 0000000000..4012dc8cfa
--- /dev/null
+++ b/tests/playwright/helpers/utils.ts
@@ -0,0 +1,34 @@
+import { expect, Page } from '@playwright/test'
+
+import { DatabaseAPIRequests } from './api/api-databases'
+import { ossStandaloneConfig } from './conf'
+
+export async function addStandaloneInstanceAndNavigateToIt(
+ page: Page,
+ databaseService: DatabaseAPIRequests,
+): Promise<() => Promise> {
+ // Add a new standalone database
+ databaseService.addNewStandaloneDatabaseApi(ossStandaloneConfig)
+
+ page.reload()
+
+ return async function cleanup() {
+ try {
+ await databaseService.deleteStandaloneDatabaseApi(
+ ossStandaloneConfig,
+ )
+ } catch (error) {
+ console.warn('Error during cleanup:', error)
+ }
+ }
+}
+
+export async function navigateToStandaloneInstance(page: Page): Promise {
+ // Click on the added database
+ const dbItems = page.locator('[data-testid^="instance-name"]')
+ const db = dbItems.filter({
+ hasText: ossStandaloneConfig.databaseName.trim(),
+ })
+ await expect(db).toBeVisible({ timeout: 5000 })
+ await db.first().click()
+}
diff --git a/tests/playwright/package.json b/tests/playwright/package.json
new file mode 100644
index 0000000000..2eae9e1a7a
--- /dev/null
+++ b/tests/playwright/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "playwright",
+ "version": "1.0.0",
+ "main": "index.js",
+ "license": "MIT",
+ "devDependencies": {
+ "@faker-js/faker": "^9.6.0",
+ "@playwright/test": "^1.52.0",
+ "@types/node": "^22.15.29",
+ "allure-commandline": "^2.33.0",
+ "allure-js-commons": "^3.2.0",
+ "allure-playwright": "^3.2.0",
+ "cross-env": "^7.0.3",
+ "nyc": "^17.1.0"
+ },
+ "scripts": {
+ "removeReportDirs": "rm -rf allure-results playwright-report test-results",
+ "allTests": "playwright test",
+ "generateReports": "allure generate --clean",
+ "test:chromium:docker": "cross-env envPath=env/.docker.env yarn playwright test --project=Chromium",
+ "test:chromium:docker:debug": "yarn test:chromium:docker --debug",
+ "test:chromium:local-web": "cross-env envPath=env/.local-web.env yarn playwright test --project=Chromium",
+ "test:chromium:local-web:debug": "yarn test:chromium:local-web --debug",
+ "test:electron": "cross-env envPath=env/.desktop.env yarn playwright test --project=Chromium",
+ "test:electron:debug": "yarn test:electron --debug",
+ "test:coverage": "cross-env COLLECT_COVERAGE=true yarn playwright test; yarn coverage",
+ "coverage": "npx nyc report --reporter=html --reporter=lcov --reporter=text",
+ "coverage:clean": "rm -rf .nyc_output coverage",
+ "clean:results": "rm -rf allure-results",
+ "prep:history": "if [ -d allure-report/history ]; then cp -R allure-report/history allure-results; fi",
+ "test:allureHistoryReport": "yarn run prep:history && yarn allTests && yarn allure generate --clean -o allure-report allure-results",
+ "test:electron:allureHistoryReport": "yarn run prep:history && yarn test:electron && yarn allure generate --clean -o allure-report allure-results",
+ "generateAndShowReports": "allure serve allure-results",
+ "test:autogen": "playwright codegen"
+ },
+ "dependencies": {
+ "axios": "^1.9.0",
+ "dotenv": "^16.4.7",
+ "dotenv-cli": "^8.0.0",
+ "fs-extra": "^11.3.0",
+ "node-color-log": "^12.0.1",
+ "sqlite3": "^5.1.7"
+ }
+}
diff --git a/tests/playwright/pageObjects/auto-discover-redis-enterprise-databases.ts b/tests/playwright/pageObjects/auto-discover-redis-enterprise-databases.ts
new file mode 100755
index 0000000000..9acbeb34c0
--- /dev/null
+++ b/tests/playwright/pageObjects/auto-discover-redis-enterprise-databases.ts
@@ -0,0 +1,36 @@
+import { Page, Locator } from '@playwright/test'
+import { BasePage } from './base-page'
+
+export class AutoDiscoverREDatabases extends BasePage {
+ // BUTTONS
+ readonly addSelectedDatabases: Locator
+
+ readonly databaseCheckbox: Locator
+
+ readonly search: Locator
+
+ readonly viewDatabasesButton: Locator
+
+ // TEXT INPUTS
+ readonly title: Locator
+
+ readonly databaseName: Locator
+
+ constructor(page: Page) {
+ super(page)
+ this.page = page
+ this.addSelectedDatabases = page.getByTestId('btn-add-databases')
+ this.databaseCheckbox = page.locator(
+ '[data-test-subj^="checkboxSelectRow"]',
+ )
+ this.search = page.getByTestId('search')
+ this.viewDatabasesButton = page.getByTestId('btn-view-databases')
+ this.title = page.getByTestId('title')
+ this.databaseName = page.locator('[data-testid^="db_name_"]')
+ }
+
+ // Get databases name
+ async getDatabaseName(): Promise {
+ return this.databaseName.textContent()
+ }
+}
diff --git a/tests/playwright/pageObjects/base-overview-page.ts b/tests/playwright/pageObjects/base-overview-page.ts
new file mode 100644
index 0000000000..3fb7401355
--- /dev/null
+++ b/tests/playwright/pageObjects/base-overview-page.ts
@@ -0,0 +1,180 @@
+/* eslint-disable no-await-in-loop */
+/* eslint-disable no-restricted-syntax */
+import { expect, Locator, Page } from '@playwright/test'
+import { Toast } from './components/common/toast'
+import { BasePage } from './base-page'
+import { RedisOverviewPage } from '../helpers/constants'
+import { DatabasesForImport } from '../types'
+
+export class BaseOverviewPage extends BasePage {
+ // Component instance used in methods
+ toast: Toast
+
+ // BUTTONS & ACTION SELECTORS
+ readonly deleteRowButton: Locator
+
+ readonly confirmDeleteButton: Locator
+
+ readonly confirmDeleteAllDbButton: Locator
+
+ // TABLE / LIST SELECTORS
+ readonly instanceRow: Locator
+
+ readonly selectAllCheckbox: Locator
+
+ readonly deleteButtonInPopover: Locator
+
+ dbNameList: Locator
+
+ readonly tableRowContent: Locator
+
+ readonly editDatabaseButton: Locator
+
+ // NAVIGATION SELECTORS
+ readonly databasePageLink: Locator
+
+ readonly rdiPageLink: Locator
+
+ // Additional – used for deletion by name
+ readonly deleteDatabaseButton: Locator
+
+ // MODULE
+ readonly moduleTooltip: Locator
+
+ constructor(page: Page) {
+ super(page)
+ this.toast = new Toast(page)
+
+ // BUTTONS & ACTION SELECTORS
+ this.deleteRowButton = page.locator('[data-testid^="delete-instance-"]')
+ this.confirmDeleteButton = page.locator(
+ '[data-testid^="delete-instance-"]',
+ { hasText: 'Remove' },
+ )
+ this.confirmDeleteAllDbButton = page.getByTestId('delete-selected-dbs')
+
+ // TABLE / LIST SELECTORS
+ this.instanceRow = page.locator('[class*=euiTableRow-isSelectable]')
+ this.selectAllCheckbox = page.locator(
+ '[data-test-subj="checkboxSelectAll"]',
+ )
+ this.deleteButtonInPopover = page.locator('#deletePopover button')
+ this.dbNameList = page.locator('[data-testid^="instance-name"]')
+ this.tableRowContent = page.locator(
+ '[data-test-subj="database-alias-column"]',
+ )
+ this.editDatabaseButton = page.locator('[data-testid^="edit-instance"]')
+
+ // NAVIGATION SELECTORS
+ this.databasePageLink = page.getByTestId('home-tab-databases')
+ this.rdiPageLink = page.getByTestId('home-tab-rdi-instances')
+
+ // Additional – we alias deleteDatabaseButton to the same as deleteRowButton
+ this.deleteDatabaseButton = page.locator(
+ '[data-testid^="delete-instance-"]',
+ )
+
+ // MODULE
+ this.moduleTooltip = page.locator('.euiToolTipPopover')
+ }
+
+ async reloadPage(): Promise {
+ await this.page.reload()
+ }
+
+ async setActivePage(type: RedisOverviewPage): Promise {
+ if (type === RedisOverviewPage.Rdi) {
+ await this.rdiPageLink.click()
+ } else {
+ await this.databasePageLink.click()
+ }
+ }
+
+ async deleteAllInstance(): Promise {
+ const count = await this.instanceRow.count()
+ if (count > 1) {
+ await this.selectAllCheckbox.click()
+ await this.deleteButtonInPopover.click()
+ await this.confirmDeleteAllDbButton.click()
+ } else if (count === 1) {
+ await this.deleteDatabaseButton.click()
+ await this.confirmDeleteButton.click()
+ }
+ if (await this.toast.toastCloseButton.isVisible()) {
+ await this.toast.toastCloseButton.click()
+ }
+ }
+
+ async deleteDatabaseByName(dbName: string): Promise {
+ const count = await this.tableRowContent.count()
+ for (let i = 0; i < count; i += 1) {
+ const text = (await this.tableRowContent.nth(i).textContent()) || ''
+ if (text.includes(dbName)) {
+ // Assumes that the delete button for the row is located at index i-1.
+ await this.deleteRowButton.nth(i - 1).click()
+ await this.confirmDeleteButton.click()
+ break
+ }
+ }
+ }
+
+ async clickOnDBByName(dbName: string): Promise {
+ const db = this.dbNameList.filter({ hasText: dbName.trim() })
+ await expect(db).toBeVisible({ timeout: 10000 })
+ await db.first().click()
+ }
+
+ async clickOnEditDBByName(databaseName: string): Promise {
+ const count = await this.dbNameList.count()
+ for (let i = 0; i < count; i += 1) {
+ const text = (await this.dbNameList.nth(i).textContent()) || ''
+ if (text.includes(databaseName)) {
+ await this.editDatabaseButton.nth(i).click()
+ break
+ }
+ }
+ }
+
+ async checkModulesInTooltip(moduleNameList: string[]): Promise {
+ for (const item of moduleNameList) {
+ await expect(
+ this.moduleTooltip.locator('span', { hasText: `${item} v.` }),
+ ).toBeVisible()
+ }
+ }
+
+ async checkModulesOnPage(moduleList: Locator[]): Promise {
+ for (const item of moduleList) {
+ await expect(item).toBeVisible()
+ }
+ }
+
+ async getAllDatabases(): Promise {
+ const databases: string[] = []
+ await expect(this.dbNameList).toBeVisible()
+ const n = await this.dbNameList.count()
+ for (let k = 0; k < n; k += 1) {
+ const name = await this.dbNameList.nth(k).textContent()
+ databases.push(name || '')
+ }
+ return databases
+ }
+
+ async compareInstances(
+ actualList: string[],
+ sortedList: string[],
+ ): Promise {
+ for (let k = 0; k < actualList.length; k += 1) {
+ await expect(actualList[k].trim()).toEqual(sortedList[k].trim())
+ }
+ }
+
+ getDatabaseNamesFromListByResult(
+ listOfDb: DatabasesForImport,
+ result: string,
+ ): string[] {
+ return listOfDb
+ .filter((element) => element.result === result)
+ .map((item) => item.name!)
+ }
+}
diff --git a/tests/playwright/pageObjects/base-page.ts b/tests/playwright/pageObjects/base-page.ts
new file mode 100644
index 0000000000..9eba42f98a
--- /dev/null
+++ b/tests/playwright/pageObjects/base-page.ts
@@ -0,0 +1,63 @@
+import { Locator, Page, expect } from '@playwright/test'
+
+export class BasePage {
+ page: Page
+
+ constructor(page: Page) {
+ this.page = page
+ }
+
+ async reload(): Promise {
+ await this.page.reload()
+ }
+
+ async navigateTo(url: string): Promise {
+ await this.page.goto(url)
+ }
+
+ async navigateToHomeUrl(): Promise {
+ await this.page.goto('/')
+ }
+
+ async click(locator: Locator): Promise {
+ await locator.click()
+ }
+
+ async fill(selector: string, value: string): Promise {
+ await this.page.fill(selector, value)
+ }
+
+ async getText(locator: Locator): Promise {
+ return locator.textContent()
+ }
+
+ async isVisible(selctor: string): Promise {
+ return this.page.locator(selctor).isVisible()
+ }
+
+ async getByTestId(testId: string): Promise {
+ return this.page.getByTestId(testId)
+ }
+
+ async waitForLocatorVisible(locator: Locator, timeout = 6000) {
+ await expect(locator).toBeVisible({ timeout })
+ }
+
+ async waitForLocatorNotVisible(locator: Locator, timeout = 6000) {
+ await expect(locator).not.toBeVisible({ timeout })
+ }
+
+ async goBackHistor(): Promise {
+ await this.page.goBack()
+ }
+
+ async elementExistsSelector(selector: string): Promise {
+ const count = await this.page.locator(selector).count()
+ return count > 0
+ }
+
+ async elementExistsLocator(locator: Locator): Promise {
+ const count = await locator.count()
+ return count > 0
+ }
+}
diff --git a/tests/playwright/pageObjects/browser-page.ts b/tests/playwright/pageObjects/browser-page.ts
new file mode 100755
index 0000000000..7e83a086bb
--- /dev/null
+++ b/tests/playwright/pageObjects/browser-page.ts
@@ -0,0 +1,1285 @@
+/* eslint-disable no-restricted-syntax */
+/* eslint-disable no-await-in-loop */
+/* eslint-disable @typescript-eslint/lines-between-class-members */
+import { expect, Locator, Page } from '@playwright/test'
+import { Toast } from './components/common/toast'
+
+import { BasePage } from './base-page'
+import { AddElementInList } from '../helpers/constants'
+
+export class BrowserPage extends BasePage {
+ private toast: Toast
+ // CSS Selectors
+ public readonly cssSelectorGrid: Locator
+ public readonly cssSelectorRows: Locator
+ public readonly cssSelectorKey: Locator
+ public readonly cssFilteringLabel: Locator
+ public readonly cssJsonValue: Locator
+ public readonly cssRowInVirtualizedTable: Locator
+ public readonly cssVirtualTableRow: Locator
+ public readonly cssKeyBadge: Locator
+ public readonly cssKeyTtl: Locator
+ public readonly cssKeySize: Locator
+ public readonly cssRemoveSuggestionItem: Locator
+
+ // BUTTONS
+ public readonly applyButton: Locator
+ public readonly cancelButton: Locator
+ public readonly deleteKeyButton: Locator
+ public readonly submitDeleteKeyButton: Locator
+ public readonly confirmDeleteKeyButton: Locator
+ public readonly editKeyTTLButton: Locator
+ public readonly refreshKeysButton: Locator
+ public readonly refreshKeyButton: Locator
+ public readonly editKeyNameButton: Locator
+ public readonly editKeyValueButton: Locator
+ public readonly closeKeyButton: Locator
+ public readonly plusAddKeyButton: Locator
+ public readonly addKeyValueItemsButton: Locator
+ public readonly saveHashFieldButton: Locator
+ public readonly saveMemberButton: Locator
+ public readonly searchButtonInKeyDetails: Locator
+ public readonly addKeyButton: Locator
+ public readonly keyTypeDropDown: Locator
+ public readonly confirmRemoveHashFieldButton: Locator
+ public readonly removeSetMemberButton: Locator
+ public readonly removeHashFieldButton: Locator
+ public readonly removeZsetMemberButton: Locator
+ public readonly confirmRemoveSetMemberButton: Locator
+ public readonly confirmRemoveZSetMemberButton: Locator
+ public readonly saveElementButton: Locator
+ public readonly removeElementFromListIconButton: Locator
+ public readonly removeElementFromListButton: Locator
+ public readonly confirmRemoveListElementButton: Locator
+ public readonly removeElementFromListSelect: Locator
+ public readonly addJsonObjectButton: Locator
+ public readonly addJsonFieldButton: Locator
+ public readonly expandJsonObject: Locator
+ public readonly scoreButton: Locator
+ public readonly sortingButton: Locator
+ public readonly editJsonObjectButton: Locator
+ public readonly applyEditButton: Locator
+ public readonly cancelEditButton: Locator
+ public readonly scanMoreButton: Locator
+ public readonly resizeBtnKeyList: Locator
+ public readonly treeViewButton: Locator
+ public readonly browserViewButton: Locator
+ public readonly searchButton: Locator
+ public readonly clearFilterButton: Locator
+ public readonly fullScreenModeButton: Locator
+ public readonly closeRightPanel: Locator
+ public readonly addNewStreamEntry: Locator
+ public readonly removeEntryButton: Locator
+ public readonly confirmRemoveEntryButton: Locator
+ public readonly clearStreamEntryInputs: Locator
+ public readonly saveGroupsButton: Locator
+ public readonly acknowledgeButton: Locator
+ public readonly confirmAcknowledgeButton: Locator
+ public readonly claimPendingMessageButton: Locator
+ public readonly submitButton: Locator
+ public readonly consumerDestinationSelect: Locator
+ public readonly removeConsumerButton: Locator
+ public readonly removeConsumerGroupButton: Locator
+ public readonly optionalParametersSwitcher: Locator
+ public readonly forceClaimCheckbox: Locator
+ public readonly editStreamLastIdButton: Locator
+ public readonly saveButton: Locator
+ public readonly bulkActionsButton: Locator
+ public readonly editHashButton: Locator
+ public readonly editHashFieldTtlButton: Locator
+ public readonly editZsetButton: Locator
+ public readonly editListButton: Locator
+ public readonly cancelStreamGroupBtn: Locator
+ public readonly patternModeBtn: Locator
+ public readonly redisearchModeBtn: Locator
+ public readonly showFilterHistoryBtn: Locator
+ public readonly clearFilterHistoryBtn: Locator
+ public readonly loadSampleDataBtn: Locator
+ public readonly executeBulkKeyLoadBtn: Locator
+ public readonly backToBrowserBtn: Locator
+ public readonly loadAllBtn: Locator
+ public readonly downloadAllValueBtn: Locator
+ public readonly openTutorialsBtn: Locator
+ public readonly keyItem: Locator
+ public readonly columnsBtn: Locator
+
+ // CONTAINERS
+ public readonly streamGroupsContainer: Locator
+ public readonly streamConsumersContainer: Locator
+ public readonly breadcrumbsContainer: Locator
+ public readonly virtualTableContainer: Locator
+ public readonly streamEntriesContainer: Locator
+ public readonly streamMessagesContainer: Locator
+ public readonly loader: Locator
+ public readonly newIndexPanel: Locator
+
+ // LINKS
+ public readonly internalLinkToWorkbench: Locator
+ public readonly userSurveyLink: Locator
+ public readonly redisearchFreeLink: Locator
+ public readonly guideLinksBtn: Locator
+
+ // OPTION ELEMENTS
+ public readonly stringOption: Locator
+ public readonly jsonOption: Locator
+ public readonly setOption: Locator
+ public readonly zsetOption: Locator
+ public readonly listOption: Locator
+ public readonly hashOption: Locator
+ public readonly streamOption: Locator
+ public readonly removeFromHeadSelection: Locator
+ public readonly filterOptionType: Locator
+ public readonly filterByKeyTypeDropDown: Locator
+ public readonly filterAllKeyType: Locator
+ public readonly consumerOption: Locator
+ public readonly claimTimeOptionSelect: Locator
+ public readonly relativeTimeOption: Locator
+ public readonly timestampOption: Locator
+ public readonly formatSwitcher: Locator
+ public readonly formatSwitcherIcon: Locator
+ public readonly refreshIndexButton: Locator
+ public readonly selectIndexDdn: Locator
+ public readonly createIndexBtn: Locator
+ public readonly cancelIndexCreationBtn: Locator
+ public readonly confirmIndexCreationBtn: Locator
+ public readonly resizeTrigger: Locator
+ public readonly filterHistoryOption: Locator
+ public readonly filterHistoryItemText: Locator
+
+ // TABS
+ public readonly streamTabGroups: Locator
+ public readonly streamTabConsumers: Locator
+ public readonly streamTabs: Locator
+
+ // TEXT INPUTS
+ public readonly addKeyNameInput: Locator
+ public readonly keyNameInput: Locator
+ public readonly keyTTLInput: Locator
+ public readonly editKeyTTLInput: Locator
+ public readonly ttlText: Locator
+ public readonly hashFieldValueInput: Locator
+ public readonly hashFieldNameInput: Locator
+ public readonly hashFieldValueEditor: Locator
+ public readonly hashTtlFieldInput: Locator
+ public readonly listKeyElementEditorInput: Locator
+ public readonly stringKeyValueInput: Locator
+ public readonly jsonKeyValueInput: Locator
+ public readonly jsonUploadInput: Locator
+ public readonly setMemberInput: Locator
+ public readonly zsetMemberScoreInput: Locator
+ public readonly filterByPatterSearchInput: Locator
+ public readonly hashFieldInput: Locator
+ public readonly hashValueInput: Locator
+ public readonly searchInput: Locator
+ public readonly jsonKeyInput: Locator
+ public readonly jsonValueInput: Locator
+ public readonly countInput: Locator
+ public readonly streamEntryId: Locator
+ public readonly streamField: Locator
+ public readonly streamValue: Locator
+ public readonly addAdditionalElement: Locator
+ public readonly streamFieldsValues: Locator
+ public readonly streamEntryIDDateValue: Locator
+ public readonly groupNameInput: Locator
+ public readonly consumerIdInput: Locator
+ public readonly streamMinIdleTimeInput: Locator
+ public readonly claimIdleTimeInput: Locator
+ public readonly claimRetryCountInput: Locator
+ public readonly lastIdInput: Locator
+ public readonly inlineItemEditor: Locator
+ public readonly indexNameInput: Locator
+ public readonly prefixFieldInput: Locator
+ public readonly indexIdentifierInput: Locator
+
+ // TEXT ELEMENTS
+ public readonly keySizeDetails: Locator
+ public readonly keyLengthDetails: Locator
+ public readonly keyNameInTheList: Locator
+ public readonly hashFieldsList: Locator
+ public readonly hashValuesList: Locator
+ public readonly hashField: Locator
+ public readonly hashFieldValue: Locator
+ public readonly setMembersList: Locator
+ public readonly zsetMembersList: Locator
+ public readonly zsetScoresList: Locator
+ public readonly listElementsList: Locator
+ public readonly jsonKeyValue: Locator
+ public readonly jsonError: Locator
+ public readonly tooltip: Locator
+ public readonly dialog: Locator
+ public readonly noResultsFound: Locator
+ public readonly noResultsFoundOnly: Locator
+ public readonly searchAdvices: Locator
+ public readonly keysNumberOfResults: Locator
+ public readonly scannedValue: Locator
+ public readonly totalKeysNumber: Locator
+ public readonly keyDetailsBadge: Locator
+ public readonly modulesTypeDetails: Locator
+ public readonly filteringLabel: Locator
+ public readonly keysSummary: Locator
+ public readonly multiSearchArea: Locator
+ public readonly keyDetailsHeader: Locator
+ public readonly keyListTable: Locator
+ public readonly keyListMessage: Locator
+ public readonly keyDetailsTable: Locator
+ public readonly keyNameFormDetails: Locator
+ public readonly keyDetailsTTL: Locator
+ public readonly progressLine: Locator
+ public readonly progressKeyList: Locator
+ public readonly jsonScalarValue: Locator
+ public readonly noKeysToDisplayText: Locator
+ public readonly streamEntryDate: Locator
+ public readonly streamEntryIdValue: Locator
+ public readonly streamFields: Locator
+ public readonly streamVirtualContainer: Locator
+ public readonly streamEntryFields: Locator
+ public readonly confirmationMessagePopover: Locator
+ public readonly streamGroupId: Locator
+ public readonly streamGroupName: Locator
+ public readonly streamMessage: Locator
+ public readonly streamConsumerName: Locator
+ public readonly consumerGroup: Locator
+ public readonly entryIdInfoIcon: Locator
+ public readonly entryIdError: Locator
+ public readonly pendingCount: Locator
+ public readonly streamRangeBar: Locator
+ public readonly rangeLeftTimestamp: Locator
+ public readonly rangeRightTimestamp: Locator
+ public readonly jsonValue: Locator
+ public readonly stringValueAsJson: Locator
+
+ // POPUPS
+ public readonly changeValueWarning: Locator
+
+ // TABLE
+ public readonly keyListItem: Locator
+
+ // DIALOG
+ public readonly noReadySearchDialogTitle: Locator
+
+ // CHECKBOXES
+ public readonly showTtlCheckbox: Locator
+ public readonly showTtlColumnCheckbox: Locator
+ public readonly showSizeColumnCheckbox: Locator
+
+ // UTILITY FUNCTIONS
+ public readonly getHashTtlFieldInput: (fieldName: string) => Locator
+ public readonly getListElementInput: (count: number) => Locator
+ public readonly getKeySize: (keyName: string) => Locator
+ public readonly getKeyTTl: (keyName: string) => Locator
+
+ constructor(page: Page) {
+ super(page)
+ this.page = page
+ this.toast = new Toast(page)
+
+ // CSS Selectors
+ this.cssSelectorGrid = page.locator('[aria-label="grid"]')
+ this.cssSelectorRows = page.locator('[aria-label="row"]')
+ this.cssSelectorKey = page.locator('[data-testid^="key-"]')
+ this.cssFilteringLabel = page.getByTestId('multi-search')
+ this.cssJsonValue = page.getByTestId('value-as-json')
+ this.cssRowInVirtualizedTable = page.locator('[role="gridcell"]')
+ this.cssVirtualTableRow = page.locator('[aria-label="row"]')
+ this.cssKeyBadge = page.locator('[data-testid^="badge-"]')
+ this.cssKeyTtl = page.locator('[data-testid^="ttl-"]')
+ this.cssKeySize = page.locator('[data-testid^="size-"]')
+ this.cssRemoveSuggestionItem = page.locator(
+ '[data-testid^="remove-suggestion-item-"]',
+ )
+
+ // BUTTONS
+ this.applyButton = page.getByTestId('apply-btn')
+ this.cancelButton = page.getByTestId('cancel-btn')
+ this.deleteKeyButton = page.getByTestId('delete-key-btn')
+ this.submitDeleteKeyButton = page.getByTestId('submit-delete-key')
+ this.confirmDeleteKeyButton = page.getByTestId('delete-key-confirm-btn')
+ this.editKeyTTLButton = page.getByTestId('edit-ttl-btn')
+ this.refreshKeysButton = page.getByTestId('keys-refresh-btn')
+ this.refreshKeyButton = page.getByTestId('key-refresh-btn')
+ this.editKeyNameButton = page.getByTestId('edit-key-btn')
+ this.editKeyValueButton = page.getByTestId('edit-key-value-btn')
+ this.closeKeyButton = page.getByTestId('close-key-btn')
+ this.plusAddKeyButton = page.getByTestId('btn-add-key')
+ this.addKeyValueItemsButton = page.getByTestId(
+ 'add-key-value-items-btn',
+ )
+ this.saveHashFieldButton = page.getByTestId('save-fields-btn')
+ this.saveMemberButton = page.getByTestId('save-members-btn')
+ this.searchButtonInKeyDetails = page.getByTestId('search-button')
+ this.addKeyButton = page.locator('button', {
+ hasText: /^Add Key$/,
+ })
+ this.keyTypeDropDown = page.locator(
+ 'fieldset button.euiSuperSelectControl',
+ )
+ this.confirmRemoveHashFieldButton = page.locator(
+ '[data-testid^="remove-hash-button-"] span',
+ )
+ this.removeSetMemberButton = page.getByTestId('set-remove-btn')
+ this.removeHashFieldButton = page.getByTestId('remove-hash-button')
+ this.removeZsetMemberButton = page.getByTestId('zset-remove-button')
+ this.confirmRemoveSetMemberButton = page.locator(
+ '[data-testid^="set-remove-btn-"] span',
+ )
+ this.confirmRemoveZSetMemberButton = page.locator(
+ '[data-testid^="zset-remove-button-"] span',
+ )
+ this.saveElementButton = page.getByTestId('save-elements-btn')
+ this.removeElementFromListIconButton = page.getByTestId(
+ 'remove-key-value-items-btn',
+ )
+ this.removeElementFromListButton = page.getByTestId(
+ 'remove-elements-btn',
+ )
+ this.confirmRemoveListElementButton = page.getByTestId('remove-submit')
+ this.removeElementFromListSelect =
+ page.getByTestId('destination-select')
+ this.addJsonObjectButton = page.getByTestId('add-object-btn')
+ this.addJsonFieldButton = page.getByTestId('add-field-btn')
+ this.expandJsonObject = page.getByTestId('expand-object')
+ this.scoreButton = page.getByTestId('score-button')
+ this.sortingButton = page.getByTestId('header-sorting-button')
+ this.editJsonObjectButton = page.getByTestId('edit-json-field')
+ this.applyEditButton = page.getByTestId('apply-edit-btn')
+ this.cancelEditButton = page.getByTestId('cancel-edit-btn')
+ this.scanMoreButton = page.getByTestId('scan-more')
+ this.resizeBtnKeyList = page.locator(
+ '[data-test-subj="resize-btn-keyList-keyDetails"]',
+ )
+ this.treeViewButton = page.getByTestId('view-type-list-btn')
+ this.browserViewButton = page.getByTestId('view-type-browser-btn')
+ this.searchButton = page.getByTestId('search-btn')
+ this.clearFilterButton = page.getByTestId('reset-filter-btn')
+ this.fullScreenModeButton = page.getByTestId('toggle-full-screen')
+ this.closeRightPanel = page.getByTestId('close-right-panel-btn')
+ this.addNewStreamEntry = page.getByTestId('add-key-value-items-btn')
+ this.removeEntryButton = page.locator(
+ '[data-testid^="remove-entry-button-"]',
+ )
+ this.confirmRemoveEntryButton = page
+ .locator('[data-testid^="remove-entry-button-"]')
+ .filter({ hasText: 'Remove' })
+ this.clearStreamEntryInputs = page.getByTestId('remove-item')
+ this.saveGroupsButton = page.getByTestId('save-groups-btn')
+ this.acknowledgeButton = page.getByTestId('acknowledge-btn')
+ this.confirmAcknowledgeButton = page.getByTestId('acknowledge-submit')
+ this.claimPendingMessageButton = page.getByTestId(
+ 'claim-pending-message',
+ )
+ this.submitButton = page.getByTestId('btn-submit')
+ this.consumerDestinationSelect = page.getByTestId('destination-select')
+ this.removeConsumerButton = page.locator(
+ '[data-testid^="remove-consumer-button"]',
+ )
+ this.removeConsumerGroupButton = page.locator(
+ '[data-testid^="remove-groups-button"]',
+ )
+ this.optionalParametersSwitcher = page.getByTestId(
+ 'optional-parameters-switcher',
+ )
+ this.forceClaimCheckbox = page
+ .getByTestId('force-claim-checkbox')
+ .locator('..')
+ this.editStreamLastIdButton = page.getByTestId('stream-group_edit-btn')
+ this.saveButton = page.getByTestId('save-btn')
+ this.bulkActionsButton = page.getByTestId('btn-bulk-actions')
+ this.editHashButton = page.locator('[data-testid^="hash_edit-btn-"]')
+ this.editHashFieldTtlButton = page.locator(
+ '[data-testid^="hash-ttl_edit-btn-"]',
+ )
+ this.editZsetButton = page.locator('[data-testid^="zset_edit-btn-"]')
+ this.editListButton = page.locator('[data-testid^="list_edit-btn-"]')
+ this.cancelStreamGroupBtn = page.getByTestId('cancel-stream-groups-btn')
+ this.patternModeBtn = page.getByTestId('search-mode-pattern-btn')
+ this.redisearchModeBtn = page.getByTestId('search-mode-redisearch-btn')
+ this.showFilterHistoryBtn = page.getByTestId('show-suggestions-btn')
+ this.clearFilterHistoryBtn = page.getByTestId('clear-history-btn')
+ this.loadSampleDataBtn = page.getByTestId('load-sample-data-btn')
+ this.executeBulkKeyLoadBtn = page.getByTestId(
+ 'load-sample-data-btn-confirm',
+ )
+ this.backToBrowserBtn = page.getByTestId('back-right-panel-btn')
+ this.loadAllBtn = page.getByTestId('load-all-value-btn')
+ this.downloadAllValueBtn = page.getByTestId('download-all-value-btn')
+ this.openTutorialsBtn = page.getByTestId('explore-msg-btn')
+ this.keyItem = page.locator(
+ '[data-testid*="node-item"][data-testid*="keys:"]',
+ )
+ this.columnsBtn = page.getByTestId('btn-columns-actions')
+
+ // CONTAINERS
+ this.streamGroupsContainer = page.getByTestId('stream-groups-container')
+ this.streamConsumersContainer = page.getByTestId(
+ 'stream-consumers-container',
+ )
+ this.breadcrumbsContainer = page.getByTestId('breadcrumbs-container')
+ this.virtualTableContainer = page.getByTestId('virtual-table-container')
+ this.streamEntriesContainer = page.getByTestId(
+ 'stream-entries-container',
+ )
+ this.streamMessagesContainer = page.getByTestId(
+ 'stream-messages-container',
+ )
+ this.loader = page.getByTestId('type-loading')
+ this.newIndexPanel = page.getByTestId('create-index-panel')
+
+ // LINKS
+ this.internalLinkToWorkbench = page.getByTestId(
+ 'internal-workbench-link',
+ )
+ this.userSurveyLink = page.getByTestId('user-survey-link')
+ this.redisearchFreeLink = page.getByTestId('get-started-link')
+ this.guideLinksBtn = page.locator('[data-testid^="guide-button-"]')
+
+ // OPTION ELEMENTS
+ this.stringOption = page.locator('#string')
+ this.jsonOption = page.locator('#ReJSON-RL')
+ this.setOption = page.locator('#set')
+ this.zsetOption = page.locator('#zset')
+ this.listOption = page.locator('#list')
+ this.hashOption = page.locator('#hash')
+ this.streamOption = page.locator('#stream')
+ this.removeFromHeadSelection = page.locator('#HEAD')
+ this.filterOptionType = page.locator(
+ '[data-test-subj^="filter-option-type-"]',
+ )
+ this.filterByKeyTypeDropDown = page.getByTestId(
+ 'select-filter-key-type',
+ )
+ this.filterAllKeyType = page.locator('#all')
+ this.consumerOption = page.getByTestId('consumer-option')
+ this.claimTimeOptionSelect = page.getByTestId('time-option-select')
+ this.relativeTimeOption = page.locator('#idle')
+ this.timestampOption = page.locator('#time')
+ this.formatSwitcher = page.getByTestId('select-format-key-value')
+ this.formatSwitcherIcon = page.locator(
+ '[data-testid^="key-value-formatter-option-selected"]',
+ )
+ this.refreshIndexButton = page.getByTestId('refresh-indexes-btn')
+ this.selectIndexDdn = page.locator(
+ '[data-testid="select-index-placeholder"],[data-testid="select-search-mode"]',
+ )
+ this.createIndexBtn = page.getByTestId('create-index-btn')
+ this.cancelIndexCreationBtn = page.getByTestId(
+ 'create-index-cancel-btn',
+ )
+ this.confirmIndexCreationBtn = page.getByTestId('create-index-btn')
+ this.resizeTrigger = page.locator('[data-testid^="resize-trigger-"]')
+ this.filterHistoryOption = page.getByTestId('suggestion-item-')
+ this.filterHistoryItemText = page.getByTestId('suggestion-item-text')
+
+ // TABS
+ this.streamTabGroups = page.getByTestId('stream-tab-Groups')
+ this.streamTabConsumers = page.getByTestId('stream-tab-Consumers')
+ this.streamTabs = page.locator('[data-test-subj="stream-tabs"]')
+
+ // TEXT INPUTS
+ this.addKeyNameInput = page.getByTestId('key')
+ this.keyNameInput = page.getByTestId('edit-key-input')
+ this.keyTTLInput = page.getByTestId('ttl')
+ this.editKeyTTLInput = page.getByTestId('edit-ttl-input')
+ this.ttlText = page.getByTestId('key-ttl-text').locator('span')
+ this.hashFieldValueInput = page.getByTestId('field-value')
+ this.hashFieldNameInput = page.getByTestId('field-name')
+ this.hashFieldValueEditor = page.getByTestId('hash_value-editor')
+ this.hashTtlFieldInput = page.getByTestId('hash-ttl')
+ this.listKeyElementEditorInput = page.getByTestId('list_value-editor-')
+ this.stringKeyValueInput = page.getByTestId('string-value')
+ this.jsonKeyValueInput = page.locator(
+ 'div[data-mode-id=json] textarea',
+ )
+ this.jsonUploadInput = page.getByTestId('upload-input-file')
+ this.setMemberInput = page.getByTestId('member-name')
+ this.zsetMemberScoreInput = page.getByTestId('member-score')
+ this.filterByPatterSearchInput = page.getByTestId('search-key')
+ this.hashFieldInput = page.getByTestId('hash-field')
+ this.hashValueInput = page.getByTestId('hash-value')
+ this.searchInput = page.getByTestId('search')
+ this.jsonKeyInput = page.getByTestId('json-key')
+ this.jsonValueInput = page.getByTestId('json-value')
+ this.countInput = page.getByTestId('count-input')
+ this.streamEntryId = page.getByTestId('entryId')
+ this.streamField = page.getByTestId('field-name')
+ this.streamValue = page.getByTestId('field-value')
+ this.addAdditionalElement = page.getByTestId('add-item')
+ this.streamFieldsValues = page.getByTestId('stream-entry-field-')
+ this.streamEntryIDDateValue = page.locator(
+ '[data-testid^="stream-entry-"][data-testid$="date"]',
+ )
+ this.groupNameInput = page.getByTestId('group-name-field')
+ this.consumerIdInput = page.getByTestId('id-field')
+ this.streamMinIdleTimeInput = page.getByTestId('min-idle-time')
+ this.claimIdleTimeInput = page.getByTestId('time-count')
+ this.claimRetryCountInput = page.getByTestId('retry-count')
+ this.lastIdInput = page.getByTestId('last-id-field')
+ this.inlineItemEditor = page.getByTestId('inline-item-editor')
+ this.indexNameInput = page.getByTestId('index-name')
+ this.prefixFieldInput = page.locator('[data-test-subj="comboBoxInput"]')
+ this.indexIdentifierInput = page.getByTestId('identifier-')
+
+ // TEXT ELEMENTS
+ this.keySizeDetails = page.getByTestId('key-size-text')
+ this.keyLengthDetails = page.getByTestId('key-length-text')
+ this.keyNameInTheList = this.cssSelectorKey
+ this.hashFieldsList = page.getByTestId('hash-field-').locator('span')
+ this.hashValuesList = page
+ .getByTestId('hash_content-value-')
+ .locator('span')
+ this.hashField = page.getByTestId('hash-field-').first()
+ this.hashFieldValue = page.getByTestId('hash_content-value-')
+ this.setMembersList = page.getByTestId('set-member-value-')
+ this.zsetMembersList = page.getByTestId('zset-member-value-')
+ this.zsetScoresList = page.getByTestId('zset_content-value-')
+ this.listElementsList = page.getByTestId('list_content-value-')
+ this.jsonKeyValue = page.getByTestId('json-data')
+ this.jsonError = page.getByTestId('edit-json-error')
+ this.tooltip = page.locator('[role="tooltip"]')
+ this.dialog = page.locator('[role="dialog"]')
+ this.noResultsFound = page.locator('[data-test-subj="no-result-found"]')
+ this.noResultsFoundOnly = page.getByTestId('no-result-found-only')
+ this.searchAdvices = page.locator('[data-test-subj="search-advices"]')
+ this.keysNumberOfResults = page.getByTestId('keys-number-of-results')
+ this.scannedValue = page.getByTestId('keys-number-of-scanned')
+ this.totalKeysNumber = page.getByTestId('keys-total')
+ this.keyDetailsBadge = page.locator(
+ '.key-details-header .euiBadge__text',
+ )
+ this.modulesTypeDetails = page.getByTestId('modules-type-details')
+ this.filteringLabel = page.getByTestId('badge-')
+ this.keysSummary = page.getByTestId('keys-summary')
+ this.multiSearchArea = page.getByTestId('multi-search')
+ this.keyDetailsHeader = page.getByTestId('key-details-header')
+ this.keyListTable = page.getByTestId('keyList-table')
+ this.keyListMessage = page.getByTestId('no-result-found-msg')
+ this.keyDetailsTable = page.getByTestId('key-details')
+ this.keyNameFormDetails = page.getByTestId('key-name-text')
+ this.keyDetailsTTL = page.getByTestId('key-ttl-text')
+ this.progressLine = page.locator('div.euiProgress')
+ this.progressKeyList = page.getByTestId('progress-key-list')
+ this.jsonScalarValue = page.getByTestId('json-scalar-value')
+ this.noKeysToDisplayText = page.getByTestId('no-result-found-msg')
+ this.streamEntryDate = page.locator(
+ '[data-testid*="-date"][data-testid*="stream-entry"]',
+ )
+ this.streamEntryIdValue = page.locator(
+ '.streamItemId[data-testid*="stream-entry"]',
+ )
+ this.streamFields = page.locator(
+ '[data-test-subj="stream-entries-container"] .truncateText',
+ )
+ this.streamVirtualContainer = page
+ .locator('[data-testid="virtual-grid-container"] div div')
+ .first()
+ this.streamEntryFields = page.getByTestId('stream-entry-field')
+ this.confirmationMessagePopover = page.locator(
+ 'div.euiPopover__panel .euiText',
+ )
+ this.streamGroupId = page
+ .locator('.streamItemId[data-testid^="stream-group-id"]')
+ .first()
+ this.streamGroupName = page.getByTestId('stream-group-name')
+ this.streamMessage = page.locator(
+ '[data-testid*="-date"][data-testid^="stream-message"]',
+ )
+ this.streamConsumerName = page.getByTestId('stream-consumer-')
+ this.consumerGroup = page.getByTestId('stream-group-')
+ this.entryIdInfoIcon = page.getByTestId('entry-id-info-icon')
+ this.entryIdError = page.getByTestId('id-error')
+ this.pendingCount = page.getByTestId('pending-count')
+ this.streamRangeBar = page.getByTestId('mock-fill-range')
+ this.rangeLeftTimestamp = page.getByTestId('range-left-timestamp')
+ this.rangeRightTimestamp = page.getByTestId('range-right-timestamp')
+ this.jsonValue = page.getByTestId('value-as-json')
+ this.stringValueAsJson = page.getByTestId('value-as-json')
+
+ // POPUPS
+ this.changeValueWarning = page.getByTestId('approve-popover')
+
+ // TABLE
+ this.keyListItem = page.locator('[role="rowgroup"] [role="row"]')
+
+ // DIALOG
+ this.noReadySearchDialogTitle = page.getByTestId('welcome-page-title')
+
+ // CHECKBOXES
+ this.showTtlCheckbox = page.getByTestId('test-check-ttl').locator('..')
+ this.showTtlColumnCheckbox = page.getByTestId('show-ttl').locator('..')
+ this.showSizeColumnCheckbox = page
+ .getByTestId('show-key-size')
+ .locator('..')
+
+ // UTILITY FUNCTIONS
+ this.getHashTtlFieldInput = (fieldName: string): Locator =>
+ page.getByTestId(`hash-ttl_content-value-${fieldName}`)
+ this.getListElementInput = (count: number): Locator =>
+ page.locator(`[data-testid*="element-${count}"]`)
+ this.getKeySize = (keyName: string): Locator =>
+ page.getByTestId(`size-${keyName}`)
+ this.getKeyTTl = (keyName: string): Locator =>
+ page.getByTestId(`ttl-${keyName}`)
+ }
+
+ async commonAddNewKey(keyName: string, TTL?: string): Promise {
+ await this.waitForLocatorNotVisible(this.progressLine)
+ await this.waitForLocatorNotVisible(this.loader)
+ await this.plusAddKeyButton.click()
+ await this.addKeyNameInput.click()
+ await this.addKeyNameInput.fill(keyName, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ if (TTL !== undefined) {
+ await this.keyTTLInput.click()
+ await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })
+ }
+ await this.keyTypeDropDown.click()
+ }
+
+ async addStringKey(
+ keyName: string,
+ value = ' ',
+ TTL?: string,
+ ): Promise {
+ await this.plusAddKeyButton.click()
+ await this.keyTypeDropDown.click()
+ await this.stringOption.click()
+ await this.addKeyNameInput.click()
+ await this.addKeyNameInput.fill(keyName, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ if (TTL !== undefined) {
+ await this.keyTTLInput.click()
+ await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })
+ }
+ await this.stringKeyValueInput.click()
+ await this.stringKeyValueInput.fill(value, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.addKeyButton.click()
+ }
+
+ async addJsonKey(
+ keyName: string,
+ value: string,
+ TTL?: string,
+ ): Promise {
+ await this.plusAddKeyButton.click()
+ await this.keyTypeDropDown.click()
+ await this.jsonOption.click()
+ await this.addKeyNameInput.click()
+ await this.addKeyNameInput.fill(keyName, {
+ timeout: 0,
+ })
+ await this.jsonKeyValueInput.click()
+ await this.jsonKeyValueInput.fill(value, {
+ timeout: 0,
+ })
+ if (TTL !== undefined) {
+ await this.keyTTLInput.click()
+ await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })
+ }
+ await this.addKeyButton.click()
+ }
+
+ async addSetKey(keyName: string, TTL = ' ', members = ' '): Promise {
+ if (await this.toast.toastCloseButton.isVisible()) {
+ await this.toast.toastCloseButton.click()
+ }
+ await this.waitForLocatorNotVisible(this.progressLine)
+ await this.waitForLocatorNotVisible(this.loader)
+ await this.plusAddKeyButton.click()
+ await this.keyTypeDropDown.click()
+ await this.setOption.click()
+ await this.addKeyNameInput.click()
+ await this.addKeyNameInput.fill(keyName, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.keyTTLInput.click()
+ await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })
+ await this.setMemberInput.fill(members, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.addKeyButton.click()
+ await this.toast.closeToast()
+ }
+
+ async addZSetKey(
+ keyName: string,
+ scores = ' ',
+ TTL = ' ',
+ members = ' ',
+ ): Promise {
+ await this.waitForLocatorNotVisible(this.progressLine)
+ await this.waitForLocatorNotVisible(this.loader)
+ await this.plusAddKeyButton.click()
+ await this.keyTypeDropDown.click()
+ await this.zsetOption.click()
+ await this.addKeyNameInput.click()
+ await this.addKeyNameInput.fill(keyName, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.keyTTLInput.click()
+ await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })
+ await this.setMemberInput.fill(members, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.zsetMemberScoreInput.fill(scores, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.addKeyButton.click()
+ }
+
+ async addListKey(
+ keyName: string,
+ TTL = ' ',
+ element: string[] = [' '],
+ position: AddElementInList = AddElementInList.Tail,
+ ): Promise {
+ await this.waitForLocatorNotVisible(this.progressLine)
+ await this.waitForLocatorNotVisible(this.loader)
+ await this.plusAddKeyButton.click()
+ await this.keyTypeDropDown.click()
+ await this.listOption.click()
+ await this.addKeyNameInput.click()
+ await this.addKeyNameInput.fill(keyName, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.keyTTLInput.click()
+ await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })
+ if (position === AddElementInList.Head) {
+ await this.removeElementFromListSelect.click()
+ await this.removeFromHeadSelection.click()
+ await expect(this.removeFromHeadSelection).not.toBeVisible()
+ }
+ for (let i = 0; i < element.length; i += 1) {
+ await this.getListElementInput(i).click()
+ await this.getListElementInput(i).fill(element[i], {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ if (element.length > 1 && i < element.length - 1) {
+ await this.addAdditionalElement.click()
+ }
+ }
+ await this.addKeyButton.click()
+ }
+
+ async addHashKey(
+ keyName: string,
+ TTL = ' ',
+ field = ' ',
+ value = ' ',
+ fieldTtl = '',
+ ): Promise {
+ if (await this.toast.isCloseButtonVisible()) {
+ await this.toast.closeToast()
+ }
+ await this.waitForLocatorNotVisible(this.progressLine)
+ await this.waitForLocatorNotVisible(this.loader)
+ await this.plusAddKeyButton.click()
+ await this.keyTypeDropDown.click()
+ await this.hashOption.click()
+ await this.addKeyNameInput.click()
+ await this.addKeyNameInput.fill(keyName, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.keyTTLInput.click()
+ await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })
+ await this.hashFieldNameInput.fill(field, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.hashFieldValueInput.fill(value, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ if (fieldTtl !== '') {
+ await this.hashTtlFieldInput.fill(fieldTtl, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ }
+ await this.addKeyButton.click()
+ await this.toast.closeToast()
+ }
+
+ async addStreamKey(
+ keyName: string,
+ field: string,
+ value: string,
+ TTL?: string,
+ ): Promise {
+ await this.commonAddNewKey(keyName, TTL)
+ await this.streamOption.click()
+ await expect(this.streamEntryId).toHaveValue('*', { timeout: 5000 })
+ await this.streamField.fill(field, { timeout: 0, noWaitAfter: false })
+ await this.streamValue.fill(value, { timeout: 0, noWaitAfter: false })
+ await expect(this.addKeyButton).not.toBeDisabled()
+ await this.addKeyButton.click()
+ await this.toast.closeToast()
+ }
+
+ async addEntryToStream(
+ field: string,
+ value: string,
+ entryId?: string,
+ ): Promise {
+ await this.addNewStreamEntry.click()
+ await this.streamField.fill(field, { timeout: 0, noWaitAfter: false })
+ await this.streamValue.fill(value, { timeout: 0, noWaitAfter: false })
+ if (entryId !== undefined) {
+ await this.streamEntryId.fill(entryId, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ }
+ await this.saveElementButton.click()
+ await expect(this.streamEntriesContainer).toContainText(field)
+ await expect(this.streamEntriesContainer).toContainText(value)
+ }
+
+ async fulfillSeveralStreamFields(
+ fields: string[],
+ values: string[],
+ entryId?: string,
+ ): Promise {
+ for (let i = 0; i < fields.length; i += 1) {
+ await this.streamField
+ .nth(-1)
+ .fill(fields[i], { timeout: 0, noWaitAfter: false })
+ await this.streamValue
+ .nth(-1)
+ .fill(values[i], { timeout: 0, noWaitAfter: false })
+ if (i < fields.length - 1) {
+ await this.addAdditionalElement.click()
+ }
+ }
+ if (entryId !== undefined) {
+ await this.streamEntryId.fill(entryId, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ }
+ }
+
+ async selectFilterGroupType(groupName: string): Promise {
+ await this.filterByKeyTypeDropDown.click()
+ await this.filterOptionType.locator(groupName).click()
+ }
+
+ async setAllKeyType(): Promise {
+ await this.filterByKeyTypeDropDown.click()
+ await this.filterAllKeyType.click()
+ }
+
+ async searchByKeyName(keyName: string): Promise {
+ await this.filterByPatterSearchInput.click()
+ await this.filterByPatterSearchInput.fill(keyName, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.page.keyboard.press('Enter')
+ }
+
+ getKeySelectorByName(keyName: string): Locator {
+ return this.page.locator(`[data-testid="key-${keyName}"]`)
+ }
+
+ async isKeyIsDisplayedInTheList(keyName: string): Promise {
+ const keyNameInTheList = this.getKeySelectorByName(keyName)
+ await this.waitForLocatorNotVisible(this.loader)
+ return keyNameInTheList.isVisible()
+ }
+
+ async deleteKey(): Promise {
+ if (await this.toast.toastCloseButton.isVisible()) {
+ await this.toast.toastCloseButton.click()
+ }
+ await this.keyNameInTheList.click()
+ await this.deleteKeyButton.click()
+ await this.confirmDeleteKeyButton.click()
+ }
+
+ async deleteKeyByName(keyName: string): Promise {
+ await this.searchByKeyName(keyName)
+ await this.keyNameInTheList.hover()
+ await this.keyNameInTheList.click()
+ await this.deleteKeyButton.click()
+ await this.confirmDeleteKeyButton.click()
+ }
+
+ async deleteKeysByNames(keyNames: string[]): Promise {
+ for (const name of keyNames) {
+ await this.deleteKeyByName(name)
+ }
+ }
+
+ async deleteKeyByNameFromList(keyName: string): Promise {
+ await this.searchByKeyName(keyName)
+ await this.keyNameInTheList.hover()
+ await this.page
+ .locator(`[data-testid="delete-key-btn-${keyName}"]`)
+ .click()
+ await this.submitDeleteKeyButton.click()
+ }
+
+ async editKeyName(keyName: string): Promise {
+ await this.editKeyNameButton.click()
+ await this.keyNameInput.fill(keyName, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.applyButton.click()
+ }
+
+ async editStringKeyValue(value: string): Promise {
+ await this.stringKeyValueInput.click()
+ await this.stringKeyValueInput.fill(value, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.applyButton.click()
+ }
+
+ async getStringKeyValue(): Promise {
+ return this.stringKeyValueInput.textContent()
+ }
+
+ async getZsetKeyScore(): Promise {
+ return this.zsetScoresList.textContent()
+ }
+
+ async addFieldToHash(
+ keyFieldValue: string,
+ keyValue: string,
+ fieldTtl = '',
+ ): Promise {
+ if (await this.toast.toastCloseButton.isVisible()) {
+ await this.toast.toastCloseButton.click()
+ }
+ await this.addKeyValueItemsButton.click()
+ await this.hashFieldInput.fill(keyFieldValue, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.hashValueInput.fill(keyValue, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ if (fieldTtl !== '') {
+ await this.hashTtlFieldInput.fill(fieldTtl, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ }
+ await this.saveHashFieldButton.click()
+ }
+
+ async editHashKeyValue(value: string): Promise {
+ await this.hashFieldValue.hover()
+ await this.editHashButton.click()
+ await this.hashFieldValueEditor.fill(value, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.applyButton.click()
+ }
+
+ async editHashFieldTtlValue(
+ fieldName: string,
+ fieldTtl: string,
+ ): Promise {
+ await this.getHashTtlFieldInput(fieldName).hover()
+ await this.editHashFieldTtlButton.click()
+ await this.inlineItemEditor.fill(fieldTtl, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.applyButton.click()
+ }
+
+ async getHashKeyValue(): Promise {
+ return this.hashFieldValue.textContent()
+ }
+
+ async editListKeyValue(value: string): Promise {
+ await this.listElementsList.hover()
+ await this.editListButton.click()
+ await this.listKeyElementEditorInput.fill(value, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.applyButton.click()
+ }
+
+ async getListKeyValue(): Promise {
+ return this.listElementsList.textContent()
+ }
+
+ async getJsonKeyValue(): Promise {
+ return this.jsonKeyValue.textContent()
+ }
+
+ async searchByTheValueInKeyDetails(value: string): Promise {
+ await this.searchButtonInKeyDetails.click()
+ await this.searchInput.fill(value, { timeout: 0, noWaitAfter: false })
+ await this.page.keyboard.press('Enter')
+ }
+
+ async secondarySearchByTheValueInKeyDetails(value: string): Promise {
+ await this.searchInput.fill(value, { timeout: 0, noWaitAfter: false })
+ await this.page.keyboard.press('Enter')
+ }
+
+ async searchByTheValueInSetKey(value: string): Promise {
+ await this.searchInput.click()
+ await this.searchInput.fill(value, { timeout: 0, noWaitAfter: false })
+ await this.page.keyboard.press('Enter')
+ }
+
+ async addMemberToSet(keyMember: string): Promise {
+ if (await this.toast.toastCloseButton.isVisible()) {
+ await this.toast.toastCloseButton.click()
+ }
+ await this.addKeyValueItemsButton.click()
+ await this.setMemberInput.fill(keyMember, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.saveMemberButton.click()
+ }
+
+ async addMemberToZSet(keyMember: string, score: string): Promise {
+ if (await this.toast.toastCloseButton.isVisible()) {
+ await this.toast.toastCloseButton.click()
+ }
+ await this.addKeyValueItemsButton.click()
+ await this.setMemberInput.fill(keyMember, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.zsetMemberScoreInput.fill(score, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.saveMemberButton.click()
+ }
+
+ async openKeyDetails(keyName: string): Promise {
+ await this.searchByKeyName(keyName)
+ await this.keyNameInTheList.click()
+ }
+
+ async openKeyDetailsByKeyName(keyName: string): Promise {
+ const keyNameInTheList = this.page.locator(
+ `[data-testid="key-${keyName}"]`,
+ )
+ await keyNameInTheList.click()
+ }
+
+ async addElementToList(
+ element: string[],
+ position: AddElementInList = AddElementInList.Tail,
+ ): Promise {
+ if (await this.toast.toastCloseButton.isVisible()) {
+ await this.toast.toastCloseButton.click()
+ }
+ await this.addKeyValueItemsButton.click()
+ if (position === AddElementInList.Head) {
+ await this.removeElementFromListSelect.click()
+ await this.removeFromHeadSelection.click()
+ await expect(this.removeFromHeadSelection).not.toBeVisible()
+ }
+ for (let i = 0; i < element.length; i += 1) {
+ await this.getListElementInput(i).click()
+ await this.getListElementInput(i).fill(element[i], {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ if (element.length > 1 && i < element.length - 1) {
+ await this.addAdditionalElement.click()
+ }
+ }
+ await this.addKeyButton.click()
+ }
+
+ async removeListElementFromHeadOld(): Promise {
+ await this.removeElementFromListIconButton.click()
+ await expect(
+ await this.countInput.getAttribute('disabled'),
+ ).toBeTruthy()
+ await this.removeElementFromListSelect.click()
+ await this.removeFromHeadSelection.click()
+ await this.removeElementFromListButton.click()
+ await this.confirmRemoveListElementButton.click()
+ }
+
+ async removeListElementFromTail(count: string): Promise {
+ await this.removeElementFromListIconButton.click()
+ await this.countInput.fill(count, { timeout: 0, noWaitAfter: false })
+ await this.removeElementFromListButton.click()
+ await this.confirmRemoveListElementButton.click()
+ }
+
+ async removeListElementFromHead(count: string): Promise {
+ await this.removeElementFromListIconButton.click()
+ await this.countInput.fill(count, { timeout: 0, noWaitAfter: false })
+ await this.removeElementFromListSelect.click()
+ await this.removeFromHeadSelection.click()
+ await this.removeElementFromListButton.click()
+ await this.confirmRemoveListElementButton.click()
+ }
+
+ async addJsonKeyOnTheSameLevel(
+ jsonKey: string,
+ jsonKeyValue: string,
+ ): Promise {
+ await this.addJsonObjectButton.click()
+ await this.jsonKeyInput.fill(jsonKey, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.jsonValueInput.fill(jsonKeyValue, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.applyButton.click()
+ }
+
+ async addJsonKeyInsideStructure(
+ jsonKey: string,
+ jsonKeyValue: string,
+ ): Promise {
+ await this.expandJsonObject.click()
+ await this.addJsonFieldButton.click()
+ await this.jsonKeyInput.fill(jsonKey, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.jsonValueInput.fill(jsonKeyValue, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.applyButton.click()
+ }
+
+ async addJsonValueInsideStructure(jsonKeyValue: string): Promise {
+ await this.expandJsonObject.click()
+ await this.addJsonFieldButton.click()
+ await this.jsonValueInput.fill(jsonKeyValue, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.applyButton.click()
+ }
+
+ async addJsonStructure(jsonStructure: string): Promise {
+ if (await this.expandJsonObject.isVisible()) {
+ await this.expandJsonObject.click()
+ }
+ await this.editJsonObjectButton.click()
+ await this.jsonValueInput.fill(jsonStructure, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ await this.applyEditButton.click()
+ }
+
+ async deleteStreamEntry(): Promise {
+ await this.removeEntryButton.click()
+ await this.confirmRemoveEntryButton.click()
+ }
+
+ async getKeyLength(): Promise {
+ const rawValue = await this.keyLengthDetails.textContent()
+ const parts = (rawValue ?? '').split(' ')
+ return parts[parts.length - 1]
+ }
+
+ async createConsumerGroup(groupName: string, id?: string): Promise {
+ await this.addKeyValueItemsButton.click()
+ await this.groupNameInput.fill(groupName, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ if (id !== undefined) {
+ await this.consumerIdInput.fill(id, {
+ timeout: 0,
+ noWaitAfter: false,
+ })
+ }
+ await this.saveGroupsButton.click()
+ }
+
+ async openStreamPendingsView(keyName: string): Promise {
+ await this.openKeyDetails(keyName)
+ await this.streamTabGroups.click()
+ await this.consumerGroup.click()
+ await this.streamConsumerName.click()
+ }
+
+ async selectFormatter(formatter: string): Promise {
+ const option = this.page.locator(
+ `[data-test-subj="format-option-${formatter}"]`,
+ )
+ await this.formatSwitcher.click()
+ await option.click()
+ }
+
+ async verifyScannningMore(): Promise {
+ for (let i = 10; i < 100; i += 10) {
+ const rememberedScanResults = Number(
+ (await this.keysNumberOfResults.textContent())?.replace(
+ /\s/g,
+ '',
+ ),
+ )
+ await expect(this.progressKeyList).not.toBeVisible({
+ timeout: 30000,
+ })
+ const scannedValueText = await this.scannedValue.textContent()
+ const regExp = new RegExp(`${i} ...`)
+ await expect(scannedValueText).toMatch(regExp)
+ await this.scanMoreButton.click()
+ const scannedResults = Number(
+ (await this.keysNumberOfResults.textContent())?.replace(
+ /\s/g,
+ '',
+ ),
+ )
+ await expect(scannedResults).toBeGreaterThan(rememberedScanResults)
+ }
+ }
+
+ async selectIndexByName(index: string): Promise {
+ const option = this.page.locator(
+ `[data-test-subj="mode-option-type-${index}"]`,
+ )
+ await this.selectIndexDdn.click()
+ await option.click()
+ }
+
+ async verifyNoKeysInDatabase(): Promise {
+ await expect(this.keyListMessage).toBeVisible()
+ await expect(this.keysSummary).not.toBeVisible()
+ }
+
+ async clearFilter(): Promise {
+ await this.clearFilterButton.click()
+ }
+
+ async clickGuideLinksByName(guide: string): Promise {
+ const linkGuide = this.page.locator(guide)
+ await linkGuide.click()
+ }
+}
diff --git a/tests/playwright/pageObjects/components/common/toast.ts b/tests/playwright/pageObjects/components/common/toast.ts
new file mode 100644
index 0000000000..d90e5731a4
--- /dev/null
+++ b/tests/playwright/pageObjects/components/common/toast.ts
@@ -0,0 +1,38 @@
+import { Locator, Page } from '@playwright/test'
+import { BasePage } from '../../base-page'
+import { ToastSelectors } from '../../../selectors'
+
+export class Toast extends BasePage {
+ public readonly toastHeader: Locator
+
+ public readonly toastBody: Locator
+
+ public readonly toastSuccess: Locator
+
+ public readonly toastError: Locator
+
+ public readonly toastCloseButton: Locator
+
+ public readonly toastSubmitBtn: Locator
+
+ public readonly toastCancelBtn: Locator
+
+ constructor(page: Page) {
+ super(page)
+ this.toastHeader = page.locator(ToastSelectors.toastHeader)
+ this.toastBody = page.locator(ToastSelectors.toastBody)
+ this.toastSuccess = page.locator(ToastSelectors.toastSuccess)
+ this.toastError = page.locator(ToastSelectors.toastError)
+ this.toastCloseButton = page.locator(ToastSelectors.toastCloseButton)
+ this.toastSubmitBtn = page.getByTestId(ToastSelectors.toastSubmitBtn)
+ this.toastCancelBtn = page.getByTestId(ToastSelectors.toastCancelBtn)
+ }
+
+ async isCloseButtonVisible(): Promise {
+ return this.isVisible(ToastSelectors.toastCloseButton)
+ }
+
+ async closeToast(): Promise {
+ await this.toastCloseButton.click()
+ }
+}
diff --git a/tests/playwright/pageObjects/components/redis-cloud-sign-in-panel.ts b/tests/playwright/pageObjects/components/redis-cloud-sign-in-panel.ts
new file mode 100644
index 0000000000..8112215bdf
--- /dev/null
+++ b/tests/playwright/pageObjects/components/redis-cloud-sign-in-panel.ts
@@ -0,0 +1,29 @@
+import { Locator, Page } from '@playwright/test'
+import { BasePage } from '../base-page'
+
+export class RedisCloudSigninPanel extends BasePage {
+ readonly ssoOauthButton: Locator
+
+ readonly ssoEmailInput: Locator
+
+ readonly submitBtn: Locator
+
+ readonly oauthAgreement: Locator
+
+ readonly googleOauth: Locator
+
+ readonly githubOauth: Locator
+
+ readonly ssoOauth: Locator
+
+ constructor(page: Page) {
+ super(page)
+ this.ssoOauthButton = page.getByTestId('sso-oauth')
+ this.ssoEmailInput = page.getByTestId('sso-email')
+ this.submitBtn = page.getByTestId('btn-submit')
+ this.oauthAgreement = page.locator('[for="ouath-agreement"]')
+ this.googleOauth = page.getByTestId('google-oauth')
+ this.githubOauth = page.getByTestId('github-oauth')
+ this.ssoOauth = page.getByTestId('sso-oauth')
+ }
+}
diff --git a/tests/playwright/pageObjects/dialogs/add-rdi-instance-dialog.ts b/tests/playwright/pageObjects/dialogs/add-rdi-instance-dialog.ts
new file mode 100755
index 0000000000..542f3c704c
--- /dev/null
+++ b/tests/playwright/pageObjects/dialogs/add-rdi-instance-dialog.ts
@@ -0,0 +1,54 @@
+import { Page, Locator } from '@playwright/test'
+import { BasePage } from '../base-page'
+
+export class AddRdiInstanceDialog extends BasePage {
+ // INPUTS
+ readonly rdiAliasInput: Locator
+
+ readonly urlInput: Locator
+
+ readonly usernameInput: Locator
+
+ readonly passwordInput: Locator
+
+ // BUTTONS
+ readonly addInstanceButton: Locator
+
+ readonly cancelInstanceBtn: Locator
+
+ readonly connectToRdiForm: Locator
+
+ // ICONS
+ readonly urlInputInfoIcon: Locator
+
+ readonly usernameInputInfoIcon: Locator
+
+ readonly passwordInputInfoIcon: Locator
+
+ constructor(page: Page) {
+ super(page)
+ this.page = page
+ this.rdiAliasInput = page.getByTestId('connection-form-name-input')
+ this.urlInput = page.getByTestId('connection-form-url-input')
+ this.usernameInput = page.getByTestId('connection-form-username-input')
+ this.passwordInput = page.getByTestId('connection-form-password-input')
+
+ this.addInstanceButton = page.getByTestId('connection-form-add-button')
+ this.cancelInstanceBtn = page.getByTestId(
+ 'connection-form-cancel-button',
+ )
+ this.connectToRdiForm = page.getByTestId('connection-form')
+
+ // Assuming that the two-level parent traversal is needed.
+ // Using an XPath locator to navigate two ancestors then find an SVG element.
+ this.urlInputInfoIcon = page
+ .getByTestId('connection-form-url-input')
+ .locator('xpath=ancestor::div[2]//svg')
+ this.usernameInputInfoIcon = page
+ .getByTestId('connection-form-username-input')
+ .locator('xpath=ancestor::div[2]//svg')
+ this.passwordInputInfoIcon = page
+ .getByTestId('connection-form-password-input')
+ .locator('xpath=ancestor::div[2]//svg')
+ }
+}
diff --git a/tests/playwright/pageObjects/dialogs/add-redis-database-dialog.ts b/tests/playwright/pageObjects/dialogs/add-redis-database-dialog.ts
new file mode 100644
index 0000000000..95256a2cdf
--- /dev/null
+++ b/tests/playwright/pageObjects/dialogs/add-redis-database-dialog.ts
@@ -0,0 +1,331 @@
+import { expect, Locator, Page } from '@playwright/test'
+import { TlsCertificates } from '../../helpers/constants'
+import { RedisCloudSigninPanel } from '../components/redis-cloud-sign-in-panel'
+import {
+ SentinelParameters,
+ AddNewDatabaseParameters,
+ SSHParameters,
+} from '../../types'
+import { BasePage } from '../base-page'
+
+export class AddRedisDatabaseDialog extends BasePage {
+ readonly redisCloudSigninPanel: RedisCloudSigninPanel
+
+ // BUTTONS
+ readonly addDatabaseButton: Locator
+
+ readonly addRedisDatabaseButton: Locator
+
+ readonly customSettingsButton: Locator
+
+ readonly addAutoDiscoverDatabase: Locator
+
+ readonly addCloudDatabaseButton: Locator
+
+ readonly redisSoftwareButton: Locator
+
+ readonly redisSentinelButton: Locator
+
+ // TEXT INPUTS
+ readonly hostInput: Locator
+
+ readonly portInput: Locator
+
+ readonly databaseAliasInput: Locator
+
+ readonly passwordInput: Locator
+
+ readonly usernameInput: Locator
+
+ readonly accessKeyInput: Locator
+
+ readonly secretKeyInput: Locator
+
+ readonly databaseIndexInput: Locator
+
+ // TABS
+ readonly generalTab: Locator
+
+ readonly securityTab: Locator
+
+ readonly decompressionTab: Locator
+
+ // DROPDOWNS
+ readonly caCertField: Locator
+
+ readonly clientCertField: Locator
+
+ readonly selectCompressor: Locator
+
+ // CHECKBOXES
+ readonly databaseIndexCheckbox: Locator
+
+ readonly useSSHCheckbox: Locator
+
+ // RADIO BUTTONS
+ readonly sshPasswordRadioBtn: Locator
+
+ readonly sshPrivateKeyRadioBtn: Locator
+
+ // LABELS
+ readonly dataCompressorLabel: Locator
+
+ // SSH TEXT INPUTS
+ readonly sshHostInput: Locator
+
+ readonly sshPortInput: Locator
+
+ readonly sshUsernameInput: Locator
+
+ readonly sshPasswordInput: Locator
+
+ readonly sshPrivateKeyInput: Locator
+
+ readonly sshPassphraseInput: Locator
+
+ // OTHER
+ readonly timeoutInput: Locator
+
+ // For certificate removal
+ aiChatMessage: Locator
+
+ aiCloseMessage: Locator
+
+ trashIconMsk(certificate: TlsCertificates): string {
+ return `[data-testid^="delete-${certificate}-cert"]`
+ }
+
+ getDeleteCertificate(certificate: TlsCertificates): Locator {
+ return this.page.locator(this.trashIconMsk(certificate))
+ }
+
+ constructor(page: Page) {
+ super(page)
+ this.page = page
+ this.redisCloudSigninPanel = new RedisCloudSigninPanel(page)
+
+ // BUTTONS
+ this.addDatabaseButton = page.locator(
+ '[data-testid^="add-redis-database"]',
+ )
+ this.addRedisDatabaseButton = page.getByTestId('btn-submit')
+ this.customSettingsButton = page.getByTestId('btn-connection-settings')
+ this.addAutoDiscoverDatabase = page.getByTestId(
+ 'add-database_tab_software',
+ )
+ this.addCloudDatabaseButton = page.getByTestId('create-free-db-btn')
+ this.redisSoftwareButton = page.getByTestId('option-btn-software')
+ this.redisSentinelButton = page.getByTestId('option-btn-sentinel')
+
+ // TEXT INPUTS
+ this.hostInput = page.getByTestId('host')
+ this.portInput = page.getByTestId('port')
+ this.databaseAliasInput = page.getByTestId('name')
+ this.passwordInput = page.getByTestId('password')
+ this.usernameInput = page.getByTestId('username')
+ this.accessKeyInput = page.getByTestId('access-key')
+ this.secretKeyInput = page.getByTestId('secret-key')
+ this.databaseIndexInput = page.getByTestId('db')
+
+ // TABS
+ this.generalTab = page.getByTestId('manual-form-tab-general')
+ this.securityTab = page.getByTestId('manual-form-tab-security')
+ this.decompressionTab = page.getByTestId(
+ 'manual-form-tab-decompression',
+ )
+
+ // DROPDOWNS
+ this.caCertField = page.getByTestId('select-ca-cert')
+ this.clientCertField = page.getByTestId('select-cert')
+ this.selectCompressor = page.getByTestId('select-compressor')
+
+ // CHECKBOXES
+ this.databaseIndexCheckbox = page.locator(
+ '[data-testid="showDb"] ~ div',
+ )
+ this.useSSHCheckbox = page.locator('[data-testid="use-ssh"] ~ div')
+
+ // RADIO BUTTONS
+ this.sshPasswordRadioBtn = page.locator('#password ~ div')
+ this.sshPrivateKeyRadioBtn = page.locator('#privateKey ~ div')
+
+ // LABELS
+ this.dataCompressorLabel = page.getByTestId(
+ '[data-testid="showCompressor"] ~ label',
+ )
+ this.aiChatMessage = page.getByTestId('ai-chat-message-btn')
+ this.aiCloseMessage = page.locator(
+ '[aria-label="Closes this modal window"]',
+ )
+
+ // SSH TEXT INPUTS
+ this.sshHostInput = page.getByTestId('sshHost')
+ this.sshPortInput = page.getByTestId('sshPort')
+ this.sshUsernameInput = page.getByTestId('sshUsername')
+ this.sshPasswordInput = page.getByTestId('sshPassword')
+ this.sshPrivateKeyInput = page.getByTestId('sshPrivateKey')
+ this.sshPassphraseInput = page.getByTestId('sshPassphrase')
+
+ // OTHER
+ this.timeoutInput = page.getByTestId('timeout')
+ }
+
+ async addRedisDataBase(
+ parameters: AddNewDatabaseParameters,
+ ): Promise {
+ await expect(this.addDatabaseButton).toBeVisible({ timeout: 10000 })
+ await this.addDatabaseButton.click()
+ await this.customSettingsButton.click()
+ await this.hostInput.fill(parameters.host)
+ await this.portInput.fill(parameters.port)
+ await this.databaseAliasInput.fill(parameters.databaseName || '')
+ if (parameters.databaseUsername) {
+ await this.usernameInput.fill(parameters.databaseUsername)
+ }
+ if (parameters.databasePassword) {
+ await this.passwordInput.fill(parameters.databasePassword)
+ }
+ }
+
+ async addLogicalRedisDatabase(
+ parameters: AddNewDatabaseParameters,
+ index: string,
+ ): Promise {
+ await this.addDatabaseButton.click()
+ await this.customSettingsButton.click()
+ await this.hostInput.fill(parameters.host)
+ await this.portInput.fill(parameters.port)
+ await this.databaseAliasInput.fill(parameters.databaseName || '')
+ if (parameters.databaseUsername) {
+ await this.usernameInput.fill(parameters.databaseUsername)
+ }
+ if (parameters.databasePassword) {
+ await this.passwordInput.fill(parameters.databasePassword)
+ }
+ await this.databaseIndexCheckbox.click()
+ await this.databaseIndexInput.fill(index)
+ await this.addRedisDatabaseButton.click()
+ }
+
+ async addStandaloneSSHDatabase(
+ databaseParameters: AddNewDatabaseParameters,
+ sshParameters: SSHParameters,
+ ): Promise {
+ await this.addDatabaseButton.click()
+ await this.customSettingsButton.click()
+ await this.hostInput.fill(databaseParameters.host)
+ await this.portInput.fill(databaseParameters.port)
+ await this.databaseAliasInput.fill(
+ databaseParameters.databaseName || '',
+ )
+ if (databaseParameters.databaseUsername) {
+ await this.usernameInput.fill(databaseParameters.databaseUsername)
+ }
+ if (databaseParameters.databasePassword) {
+ await this.passwordInput.fill(databaseParameters.databasePassword)
+ }
+ // Navigate to security tab and select SSH Tunnel checkbox
+ await this.securityTab.click()
+ await this.useSSHCheckbox.click()
+ // Fill SSH fields
+ await this.sshHostInput.fill(sshParameters.sshHost)
+ await this.sshPortInput.fill(sshParameters.sshPort)
+ await this.sshUsernameInput.fill(sshParameters.sshUsername)
+ if (sshParameters.sshPassword) {
+ await this.sshPasswordInput.fill(sshParameters.sshPassword)
+ }
+ if (sshParameters.sshPrivateKey) {
+ await this.sshPrivateKeyRadioBtn.click()
+ await this.sshPrivateKeyInput.fill(sshParameters.sshPrivateKey)
+ }
+ if (sshParameters.sshPassphrase) {
+ await this.sshPrivateKeyRadioBtn.click()
+ await this.sshPassphraseInput.fill(sshParameters.sshPassphrase)
+ }
+ await this.addRedisDatabaseButton.click()
+ }
+
+ async discoverSentinelDatabases(
+ parameters: SentinelParameters,
+ ): Promise {
+ await this.addDatabaseButton.click()
+ await this.redisSentinelButton.click()
+ if (parameters.sentinelHost) {
+ await this.hostInput.fill(parameters.sentinelHost)
+ }
+ if (parameters.sentinelPort) {
+ await this.portInput.fill(parameters.sentinelPort)
+ }
+ if (parameters.sentinelPassword) {
+ await this.passwordInput.fill(parameters.sentinelPassword)
+ }
+ }
+
+ async addAutodiscoverREClusterDatabase(
+ parameters: AddNewDatabaseParameters,
+ ): Promise {
+ await this.addDatabaseButton.click()
+ await this.redisSoftwareButton.click()
+ await this.hostInput.fill(parameters.host)
+ await this.portInput.fill(parameters.port)
+ await this.usernameInput.fill(parameters.databaseUsername || '')
+ await this.passwordInput.fill(parameters.databasePassword || '')
+ }
+
+ async addAutodiscoverRECloudDatabase(
+ cloudAPIAccessKey: string,
+ cloudAPISecretKey: string,
+ ): Promise {
+ await this.addDatabaseButton.click()
+ await this.addCloudDatabaseButton.click()
+ await this.accessKeyInput.fill(cloudAPIAccessKey)
+ await this.secretKeyInput.fill(cloudAPISecretKey)
+ }
+
+ async addOssClusterDatabase(
+ parameters: AddNewDatabaseParameters,
+ ): Promise {
+ await this.addDatabaseButton.click()
+ await this.customSettingsButton.click()
+ if (parameters.ossClusterHost) {
+ await this.hostInput.fill(parameters.ossClusterHost)
+ }
+ if (parameters.ossClusterPort) {
+ await this.portInput.fill(parameters.ossClusterPort)
+ }
+ if (parameters.ossClusterDatabaseName) {
+ await this.databaseAliasInput.fill(
+ parameters.ossClusterDatabaseName,
+ )
+ }
+ }
+
+ async setCompressorValue(compressor: string): Promise {
+ if (!(await this.selectCompressor.isVisible())) {
+ await this.dataCompressorLabel.click()
+ }
+ await this.selectCompressor.click()
+ await this.page.locator(`[id="${compressor}"]`).click()
+ }
+
+ async removeCertificateButton(
+ certificate: TlsCertificates,
+ name: string,
+ ): Promise {
+ await this.securityTab.click()
+ const row = this.page
+ .locator('button')
+ .locator('div')
+ .filter({ hasText: name })
+ const removeButtonFooter = this.page.locator(
+ '[class^="_popoverFooter"]',
+ )
+ if (certificate === TlsCertificates.CA) {
+ await this.caCertField.click()
+ } else {
+ await this.clientCertField.click()
+ }
+ await row.locator(this.trashIconMsk(certificate)).click()
+ await removeButtonFooter.locator(this.trashIconMsk(certificate)).click()
+ }
+}
diff --git a/tests/playwright/pageObjects/dialogs/user-agreement-dialog.ts b/tests/playwright/pageObjects/dialogs/user-agreement-dialog.ts
new file mode 100644
index 0000000000..1a21adb0b8
--- /dev/null
+++ b/tests/playwright/pageObjects/dialogs/user-agreement-dialog.ts
@@ -0,0 +1,65 @@
+import { expect, Locator, Page } from '@playwright/test'
+
+import { BasePage } from '../base-page'
+import { UserAgreementSelectors } from '../../selectors'
+
+export class UserAgreementDialog extends BasePage {
+ readonly userAgreementsPopup: Locator
+
+ readonly submitButton: Locator
+
+ readonly switchOptionEula: Locator
+
+ readonly switchOptionEncryption: Locator
+
+ readonly pluginSectionWithText: Locator
+
+ readonly recommendedSwitcher: Locator
+
+ constructor(page: Page) {
+ super(page)
+ this.userAgreementsPopup = page.getByTestId(
+ UserAgreementSelectors.userAgreementsPopup,
+ )
+ this.submitButton = page.getByTestId(
+ UserAgreementSelectors.submitButton,
+ )
+ this.switchOptionEula = page.getByTestId(
+ UserAgreementSelectors.switchOptionEula,
+ )
+ this.switchOptionEncryption = page.getByTestId(
+ UserAgreementSelectors.switchOptionEncryption,
+ )
+ this.pluginSectionWithText = page.getByTestId(
+ UserAgreementSelectors.pluginSectionWithText,
+ )
+ this.recommendedSwitcher = page.getByTestId(
+ UserAgreementSelectors.recommendedSwitcher,
+ )
+ }
+
+ async acceptLicenseTerms(): Promise {
+ try {
+ await this.switchOptionEula.waitFor({ timeout: 3000 }) // because the state isn't clear
+ } catch (error) {
+ // Ignore error if the dialog is not visible
+ }
+
+ if (await this.switchOptionEula.isVisible()) {
+ await this.recommendedSwitcher.click()
+ await this.switchOptionEula.click()
+ await this.submitButton.click()
+ await expect(this.userAgreementsPopup).not.toBeVisible({
+ timeout: 2000,
+ })
+ }
+ }
+
+ async getRecommendedSwitcherValue(): Promise {
+ return this.recommendedSwitcher.getAttribute('aria-checked')
+ }
+
+ async isUserAgreementDialogVisible(): Promise {
+ return this.userAgreementsPopup.isVisible()
+ }
+}
diff --git a/tests/playwright/pageObjects/index.ts b/tests/playwright/pageObjects/index.ts
new file mode 100644
index 0000000000..85b64cac53
--- /dev/null
+++ b/tests/playwright/pageObjects/index.ts
@@ -0,0 +1,10 @@
+export * from './components/common/toast'
+export * from './components/redis-cloud-sign-in-panel'
+export * from './dialogs/add-rdi-instance-dialog'
+export * from './dialogs/add-redis-database-dialog'
+export * from './dialogs/user-agreement-dialog'
+export * from './base-overview-page'
+export * from './browser-page'
+export * from './rdi-instances-list-page'
+export * from './auto-discover-redis-enterprise-databases'
+export * from './base-page'
diff --git a/tests/playwright/pageObjects/rdi-instances-list-page.ts b/tests/playwright/pageObjects/rdi-instances-list-page.ts
new file mode 100755
index 0000000000..efde53a000
--- /dev/null
+++ b/tests/playwright/pageObjects/rdi-instances-list-page.ts
@@ -0,0 +1,184 @@
+/* eslint-disable no-await-in-loop */
+import { Page, Locator, expect } from '@playwright/test'
+import { BaseOverviewPage } from './base-overview-page'
+import { AddRdiInstanceDialog } from './dialogs/add-rdi-instance-dialog'
+import { RdiInstance } from '../types'
+
+export class RdiInstancesListPage extends BaseOverviewPage {
+ readonly AddRdiInstanceDialog: AddRdiInstanceDialog
+
+ readonly addRdiInstanceButton: Locator
+
+ readonly addRdiFromEmptyListBtn: Locator
+
+ readonly quickstartBtn: Locator
+
+ readonly rdiInstanceRow: Locator
+
+ readonly emptyRdiList: Locator
+
+ readonly rdiNameList: Locator
+
+ readonly searchInput: Locator
+
+ readonly sortBy: Locator
+
+ readonly cssRdiAlias: string
+
+ readonly cssUrl: string
+
+ readonly cssRdiVersion: string
+
+ readonly cssLastConnection: string
+
+ // Assuming these selectors exist—update their locators as needed.
+ readonly deleteRowButton: Locator
+
+ readonly confirmDeleteButton: Locator
+
+ readonly editRowButton: Locator
+
+ readonly Toast: { toastCloseButton: Locator }
+
+ constructor(page: Page) {
+ super(page)
+ this.page = page
+
+ this.AddRdiInstanceDialog = new AddRdiInstanceDialog(page)
+
+ // Use getByTestId for selectors with data-testid
+ this.addRdiInstanceButton = page.getByTestId('rdi-instance')
+ this.addRdiFromEmptyListBtn = page.getByTestId('empty-rdi-instance-button')
+ this.quickstartBtn = page.getByTestId('empty-rdi-quickstart-button')
+
+ this.rdiInstanceRow = page.locator('[class*=euiTableRow-isSelectable]')
+ this.emptyRdiList = page.getByTestId('empty-rdi-instance-list')
+ this.rdiNameList = page.locator('[class*=column_name] div')
+
+ this.searchInput = page.getByTestId('search-rdi-instance-list')
+
+ // Selector using data-test-subj remains as locator
+ this.sortBy = page.locator('[data-test-subj=tableHeaderSortButton] span')
+
+ // CSS selectors (kept as string constants)
+ this.cssRdiAlias = '[data-test-subj=rdi-alias-column]'
+ this.cssUrl = '[data-testid=url]'
+ this.cssRdiVersion = '[data-test-subj=rdi-instance-version-column]'
+ this.cssLastConnection = '[data-test-subj=rdi-instance-last-connection-column]'
+
+ // These selectors are assumed. Adjust the test IDs as per your application.
+ this.deleteRowButton = page.getByTestId('delete-row-button')
+ this.confirmDeleteButton = page.getByTestId('confirm-delete-button')
+ this.editRowButton = page.getByTestId('edit-row-button')
+ this.Toast = {
+ toastCloseButton: page.getByTestId('toast-close-button')
+ }
+ }
+
+ /**
+ * Add Rdi instance.
+ * @param instanceValue Rdi instance data
+ */
+ async addRdi(instanceValue: RdiInstance): Promise {
+ await this.addRdiInstanceButton.click()
+ await this.AddRdiInstanceDialog.rdiAliasInput.fill(instanceValue.alias)
+ await this.AddRdiInstanceDialog.urlInput.fill(instanceValue.url)
+ if (instanceValue.username) {
+ await this.AddRdiInstanceDialog.usernameInput.fill(instanceValue.username)
+ }
+ if (instanceValue.password) {
+ await this.AddRdiInstanceDialog.passwordInput.fill(instanceValue.password)
+ }
+ await this.AddRdiInstanceDialog.addInstanceButton.click()
+ // Wait for the dialog to close after adding the Rdi instance
+ await this.AddRdiInstanceDialog.connectToRdiForm.waitFor({ state: 'hidden' })
+ }
+
+ /**
+ * Get Rdi instance values by index.
+ * @param index Index of Rdi instance.
+ */
+ async getRdiInstanceValuesByIndex(index: number): Promise {
+ const alias = await this.rdiInstanceRow.nth(index).locator(this.cssRdiAlias).innerText()
+ const currentLastConnection = await this.rdiInstanceRow.nth(0).locator(this.cssLastConnection).innerText()
+ const currentVersion = await this.rdiInstanceRow.nth(0).locator(this.cssRdiVersion).innerText()
+ const currentUrl = await this.rdiInstanceRow.nth(0).locator(this.cssUrl).innerText()
+
+ const rdiInstance: RdiInstance = {
+ alias,
+ url: currentUrl,
+ version: currentVersion,
+ lastConnection: currentLastConnection,
+ }
+
+ return rdiInstance
+ }
+
+ /**
+ * Delete Rdi by name.
+ * @param dbName The name of the Rdi to be deleted.
+ */
+ async deleteRdiByName(dbName: string): Promise {
+ const dbNames = this.rdiInstanceRow
+ const count = await dbNames.count()
+ for (let i = 0; i < count; i += 1) {
+ const text = await dbNames.nth(i).innerText()
+ if (text.includes(dbName)) {
+ await this.deleteRowButton.nth(i).click()
+ await this.confirmDeleteButton.click()
+ break
+ }
+ }
+ }
+
+ /**
+ * Edit Rdi by name.
+ * @param dbName The name of the Rdi to be edited.
+ */
+ async clickEditRdiByName(dbName: string): Promise {
+ const rdiNames = this.rdiInstanceRow
+ const count = await rdiNames.count()
+ for (let i = 0; i < count; i += 1) {
+ const text = await rdiNames.nth(i).innerText()
+ if (text.includes(dbName)) {
+ await this.editRowButton.nth(i).click()
+ break
+ }
+ }
+ }
+
+ /**
+ * Click Rdi by name.
+ * @param rdiName The name of the Rdi.
+ */
+ async clickRdiByName(rdiName: string): Promise {
+ if (await this.Toast.toastCloseButton.isVisible()) {
+ await this.Toast.toastCloseButton.click()
+ }
+ // Use getByText with exact match for the Rdi name
+ const rdi = this.rdiNameList.getByText(rdiName.trim(), { exact: true })
+ await expect(rdi).toBeVisible({ timeout: 3000 })
+ await rdi.click()
+ }
+
+ /**
+ * Sort Rdi list by column.
+ * @param columnName The name of the column.
+ */
+ async sortByColumn(columnName: string): Promise {
+ await this.sortBy.filter({ hasText: columnName }).click()
+ }
+
+ /**
+ * Get all Rdi aliases.
+ */
+ async getAllRdiNames(): Promise {
+ const rdis: string[] = []
+ const count = await this.rdiInstanceRow.count()
+ for (let i = 0; i < count; i += 1) {
+ const name = await this.rdiInstanceRow.nth(i).locator(this.cssRdiAlias).innerText()
+ rdis.push(name)
+ }
+ return rdis
+ }
+}
diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts
new file mode 100644
index 0000000000..cbf77fa969
--- /dev/null
+++ b/tests/playwright/playwright.config.ts
@@ -0,0 +1,127 @@
+import { defineConfig, devices } from '@playwright/test'
+import { Status } from 'allure-js-commons'
+import dotenv from 'dotenv'
+import * as os from 'os'
+
+dotenv.config({
+ path: process.env.envPath ?? 'env/.local-web.env',
+ override: true,
+})
+
+export type TestOptions = {
+ apiUrl: string
+}
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ /* Maximum time one test can run for. */
+ timeout: 600 * 1000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 5000,
+ },
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* 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,
+ /* Opt out of parallel tests on CI. */
+ // workers: process.env.CI ? 1 : undefined,
+ workers: 1,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: [
+ ['line'],
+ ['html'],
+ [
+ 'allure-playwright',
+ {
+ resultsDir: 'allure-results',
+ detail: true,
+ suiteTitle: true,
+ links: {
+ issue: {
+ nameTemplate: 'Issue #%s',
+ urlTemplate: 'https://issues.example.com/%s',
+ },
+ tms: {
+ nameTemplate: 'TMS #%s',
+ urlTemplate: 'https://tms.example.com/%s',
+ },
+ jira: {
+ urlTemplate: (v: any) =>
+ `https://jira.example.com/browse/${v}`,
+ },
+ },
+ categories: [
+ {
+ name: 'foo',
+ messageRegex: 'bar',
+ traceRegex: 'baz',
+ matchedStatuses: [Status.FAILED, Status.BROKEN],
+ },
+ ],
+ environmentInfo: {
+ os_platform: os.platform(),
+ os_release: os.release(),
+ os_version: os.version(),
+ node_version: process.version,
+ },
+ },
+ ],
+ ],
+
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ // baseURL: 'http://127.0.0.1:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ testIdAttribute: 'data-testid',
+ headless: true,
+ deviceScaleFactor: undefined,
+ viewport: { width: 1920, height: 1080 },
+ video: {
+ mode: 'on',
+ size: { width: 1920, height: 1080 },
+ },
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'Chromium',
+ testMatch: ['**.spec.ts'],
+ use: {
+ ...devices['Desktop Chrome'],
+ baseURL: process.env.COMMON_URL,
+ // headless: false,
+ launchOptions: {
+ args: [
+ '--no-sandbox',
+ '--start-maximized',
+ '--disable-dev-shm-usage',
+ '--ignore-certificate-errors',
+ '--disable-search-engine-choice-screen',
+ // '--disable-blink-features=AutomationControlled',
+ // '--disable-component-extensions-with-background-pages',
+ ],
+ },
+ },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ // webServer: {
+ // command: 'npm run start',
+ // url: 'http://127.0.0.1:3000',
+ // reuseExistingServer: !process.env.CI,
+ // },
+})
diff --git a/tests/playwright/selectors/index.ts b/tests/playwright/selectors/index.ts
new file mode 100644
index 0000000000..a9d404ae29
--- /dev/null
+++ b/tests/playwright/selectors/index.ts
@@ -0,0 +1,2 @@
+export * from './toast-selectors'
+export * from './user-agreement-selectors'
diff --git a/tests/playwright/selectors/toast-selectors.ts b/tests/playwright/selectors/toast-selectors.ts
new file mode 100644
index 0000000000..8d60edd874
--- /dev/null
+++ b/tests/playwright/selectors/toast-selectors.ts
@@ -0,0 +1,9 @@
+export const ToastSelectors = {
+ toastHeader: '[data-test-subj=euiToastHeader]',
+ toastBody: '[class*=euiToastBody]',
+ toastSuccess: '[class*=euiToast--success]',
+ toastError: '[class*=euiToast--danger]',
+ toastCloseButton: '[data-test-subj=toastCloseButton]',
+ toastSubmitBtn: 'submit-tooltip-btn',
+ toastCancelBtn: 'toast-cancel-btn',
+}
diff --git a/tests/playwright/selectors/user-agreement-selectors.ts b/tests/playwright/selectors/user-agreement-selectors.ts
new file mode 100644
index 0000000000..f0f0105c8c
--- /dev/null
+++ b/tests/playwright/selectors/user-agreement-selectors.ts
@@ -0,0 +1,8 @@
+export const UserAgreementSelectors = {
+ userAgreementsPopup: 'consents-settings-popup',
+ submitButton: 'btn-submit',
+ switchOptionEula: 'switch-option-eula',
+ switchOptionEncryption: 'switch-option-encryption',
+ pluginSectionWithText: 'plugin-section',
+ recommendedSwitcher: 'switch-option-recommended',
+}
diff --git a/tests/playwright/tests/basic-navigation.spec.ts b/tests/playwright/tests/basic-navigation.spec.ts
new file mode 100644
index 0000000000..d5414cbba2
--- /dev/null
+++ b/tests/playwright/tests/basic-navigation.spec.ts
@@ -0,0 +1,11 @@
+import { test, expect } from '../fixtures/test'
+
+test.describe('Basic Navigation and Element Visibility', () => {
+ test('should navigate to the homepage and verify title', async ({
+ page,
+ }) => {
+ const title = await page.title()
+
+ expect(title).toBe('Redis databases')
+ })
+})
diff --git a/tests/playwright/tests/keys.spec.ts b/tests/playwright/tests/keys.spec.ts
new file mode 100644
index 0000000000..b695b5bb73
--- /dev/null
+++ b/tests/playwright/tests/keys.spec.ts
@@ -0,0 +1,101 @@
+/* eslint-disable no-empty-pattern */
+import { faker } from '@faker-js/faker'
+
+import { BrowserPage } from '../pageObjects/browser-page'
+import { test, expect } from '../fixtures/test'
+import { ossStandaloneConfig } from '../helpers/conf'
+import {
+ addStandaloneInstanceAndNavigateToIt,
+ navigateToStandaloneInstance,
+} from '../helpers/utils'
+
+test.describe('Adding Database Keys', () => {
+ let browserPage: BrowserPage
+ let keyName: string
+ let cleanupInstance: () => Promise
+
+ test.beforeEach(async ({ page, api: { databaseService } }) => {
+ browserPage = new BrowserPage(page)
+ keyName = faker.string.alphanumeric(10)
+ cleanupInstance = await addStandaloneInstanceAndNavigateToIt(
+ page,
+ databaseService,
+ )
+
+ await navigateToStandaloneInstance(page)
+ })
+
+ test.afterEach(async ({ api: { keyService } }) => {
+ // Clean up: delete the key
+ await keyService.deleteKeyByNameApi(
+ keyName,
+ ossStandaloneConfig.databaseName,
+ )
+
+ await cleanupInstance()
+ })
+
+ test('Verify that user can add Hash Key', async ({}) => {
+ await browserPage.addHashKey(keyName)
+
+ // Check that new key is displayed in the list
+ await browserPage.searchByKeyName(keyName)
+ const isKeyIsDisplayedInTheList =
+ await browserPage.isKeyIsDisplayedInTheList(keyName)
+ await expect(isKeyIsDisplayedInTheList).toBe(true)
+ })
+
+ test('Verify that user can add Set Key', async ({}) => {
+ await browserPage.addSetKey(keyName)
+
+ // Check that new key is displayed in the list
+ await browserPage.searchByKeyName(keyName)
+ const isKeyIsDisplayedInTheList =
+ await browserPage.isKeyIsDisplayedInTheList(keyName)
+ await expect(isKeyIsDisplayedInTheList).toBe(true)
+ })
+
+ test('Verify that user can add List Key', async ({}) => {
+ await browserPage.addListKey(keyName)
+
+ // Check that new key is displayed in the list
+ await browserPage.searchByKeyName(keyName)
+ const isKeyIsDisplayedInTheList =
+ await browserPage.isKeyIsDisplayedInTheList(keyName)
+ await expect(isKeyIsDisplayedInTheList).toBe(true)
+ })
+
+ test('Verify that user can add String Key', async ({}) => {
+ await browserPage.addStringKey(keyName)
+
+ // Check that new key is displayed in the list
+ await browserPage.searchByKeyName(keyName)
+ const isKeyIsDisplayedInTheList =
+ await browserPage.isKeyIsDisplayedInTheList(keyName)
+ await expect(isKeyIsDisplayedInTheList).toBe(true)
+ })
+
+ test('Verify that user can add ZSet Key', async ({}) => {
+ const scores = '111'
+ await browserPage.addZSetKey(keyName, scores)
+
+ // Check that new key is displayed in the list
+ await browserPage.searchByKeyName(keyName)
+ const isKeyIsDisplayedInTheList =
+ await browserPage.isKeyIsDisplayedInTheList(keyName)
+ await expect(isKeyIsDisplayedInTheList).toBe(true)
+ })
+
+ test('Verify that user can add Stream key', async ({}) => {
+ const keyField = faker.string.alphanumeric(20)
+ const keyValue = faker.string.alphanumeric(20)
+
+ await browserPage.addStreamKey(keyName, keyField, keyValue)
+
+ // Check that new key is displayed in the list
+ await browserPage.searchByKeyName(keyName)
+ const isKeyIsDisplayedInTheList =
+ await browserPage.isKeyIsDisplayedInTheList(keyName)
+ await expect(isKeyIsDisplayedInTheList).toBe(true)
+ })
+})
diff --git a/tests/playwright/types/connections.ts b/tests/playwright/types/connections.ts
new file mode 100644
index 0000000000..ef9cd87025
--- /dev/null
+++ b/tests/playwright/types/connections.ts
@@ -0,0 +1,21 @@
+export type SentinelParameters = {
+ sentinelHost: string
+ sentinelPort: string
+ masters?: {
+ alias?: string
+ db?: string
+ name?: string
+ password?: string
+ }[]
+ sentinelPassword?: string
+ name?: string[]
+}
+
+export type SSHParameters = {
+ sshHost: string
+ sshPort: string
+ sshUsername: string
+ sshPassword?: string
+ sshPrivateKey?: string
+ sshPassphrase?: string
+}
diff --git a/tests/playwright/types/databases.ts b/tests/playwright/types/databases.ts
new file mode 100644
index 0000000000..9facc5810c
--- /dev/null
+++ b/tests/playwright/types/databases.ts
@@ -0,0 +1,84 @@
+export type DatabasesForImport = {
+ host?: string
+ port?: number | string
+ name?: string
+ result?: string
+ username?: string
+ auth?: string
+ cluster?: boolean | string
+ indName?: string
+ db?: number
+ ssh_port?: number
+ timeout_connect?: number
+ timeout_execute?: number
+ other_field?: string
+ ssl?: boolean
+ ssl_ca_cert_path?: string
+ ssl_local_cert_path?: string
+ ssl_private_key_path?: string
+}[]
+
+export type AddNewDatabaseParameters = {
+ host: string
+ port: string
+ databaseName?: string
+ databaseUsername?: string
+ databasePassword?: string
+ // For OSS Cluster parameters, you might use these fields:
+ ossClusterHost?: string
+ ossClusterPort?: string
+ ossClusterDatabaseName?: string
+ caCert?: {
+ name?: string
+ certificate?: string
+ }
+ clientCert?: {
+ name?: string
+ certificate?: string
+ key?: string
+ }
+}
+
+export type DatabaseInstance = {
+ host: string
+ port: number
+ provider?: string
+ id: string
+ connectionType?: string
+ lastConnection?: Date
+ password?: string
+ username?: string
+ name?: string
+ db?: number
+ tls?: boolean
+ ssh?: boolean
+ sshOptions?: {
+ host: string
+ port: number
+ username?: string
+ password?: string | true
+ privateKey?: string
+ passphrase?: string | true
+ }
+ tlsClientAuthRequired?: boolean
+ verifyServerCert?: boolean
+ caCert?: object
+ clientCert?: object
+ authUsername?: string
+ authPass?: string
+ isDeleting?: boolean
+ sentinelMaster?: object
+ modules: object[]
+ version: string
+ isRediStack?: boolean
+ visible?: boolean
+ loading?: boolean
+ isFreeDb?: boolean
+ tags?: {
+ id: string
+ key: string
+ value: string
+ createdAt: string
+ updatedAt: string
+ }[]
+}
diff --git a/tests/playwright/types/index.ts b/tests/playwright/types/index.ts
new file mode 100644
index 0000000000..e7a45e7df0
--- /dev/null
+++ b/tests/playwright/types/index.ts
@@ -0,0 +1,10 @@
+export * from './databases'
+export * from './connections'
+export * from './keys'
+export * from './rdi'
+
+declare global {
+ interface Window {
+ windowId?: string
+ }
+}
diff --git a/tests/playwright/types/keys.ts b/tests/playwright/types/keys.ts
new file mode 100644
index 0000000000..687b8da67c
--- /dev/null
+++ b/tests/playwright/types/keys.ts
@@ -0,0 +1,137 @@
+/**
+ * Add new keys parameters
+ * @param keyName The name of the key
+ * @param TTL The ttl of the key
+ * @param value The value of the key
+ * @param members The members of the key
+ * @param scores The scores of the key member
+ * @param field The field of the key
+ */
+export type AddNewKeyParameters = {
+ keyName: string,
+ value?: string,
+ TTL?: string,
+ members?: string,
+ scores?: string,
+ field?: string,
+ fields?: [{
+ field?: string,
+ valuse?: string
+ }]
+}
+
+/**
+ * Hash key parameters
+ * @param keyName The name of the key
+ * @param fields The Array with fields
+ * @param field The field of the field
+ * @param value The value of the field
+
+ */
+export type HashKeyParameters = {
+ keyName: string,
+ fields: {
+ field: string,
+ value: string
+ }[]
+}
+
+/**
+ * Stream key parameters
+ * @param keyName The name of the key
+ * @param entries The Array with entries
+ * @param id The id of entry
+ * @param fields The Array with fields
+ */
+export type StreamKeyParameters = {
+ keyName: string,
+ entries: {
+ id: string,
+ fields: {
+ name: string,
+ value: string
+ }[]
+ }[]
+}
+
+/**
+ * Set key parameters
+ * @param keyName The name of the key
+ * @param members The Array with members
+ */
+export type SetKeyParameters = {
+ keyName: string,
+ members: string[]
+}
+
+/**
+ * Sorted Set key parameters
+ * @param keyName The name of the key
+ * @param members The Array with members
+ * @param name The name of the member
+ * @param id The id of the member
+ */
+export type SortedSetKeyParameters = {
+ keyName: string,
+ members: {
+ name: string,
+ score: number
+ }[]
+}
+
+/**
+ * List key parameters
+ * @param keyName The name of the key
+ * @param element The element in list
+ */
+export type ListKeyParameters = {
+ keyName: string,
+ element: string
+}
+
+/**
+ * String key parameters
+ * @param keyName The name of the key
+ * @param value The value in the string
+ */
+export type StringKeyParameters = {
+ keyName: string,
+ value: string
+}
+
+/**
+ * The key arguments for multiple keys/fields adding
+ * @param keysCount The number of keys to add
+ * @param fieldsCount The number of fields in key to add
+ * @param elementsCount The number of elements in key to add
+ * @param membersCount The number of members in key to add
+ * @param keyName The full key name
+ * @param keyNameStartWith The name of key should start with
+ * @param fieldStartWitht The name of field should start with
+ * @param fieldValueStartWith The name of field value should start with
+ * @param elementStartWith The name of element should start with
+ * @param memberStartWith The name of member should start with
+ */
+
+export type AddKeyArguments = {
+ keysCount?: number,
+ fieldsCount?: number,
+ elementsCount?: number,
+ membersCount?: number,
+ keyName?: string,
+ keyNameStartWith?: string,
+ fieldStartWith?: string,
+ fieldValueStartWith?: string,
+ elementStartWith?: string,
+ memberStartWith?: string
+}
+
+/**
+ * Keys Data parameters
+ * @param textType The type of the key
+ * @param keyName The name of the key
+ */
+export type KeyData = {
+ textType: string,
+ keyName: string
+}[]
diff --git a/tests/playwright/types/rdi.ts b/tests/playwright/types/rdi.ts
new file mode 100644
index 0000000000..0c88a211f9
--- /dev/null
+++ b/tests/playwright/types/rdi.ts
@@ -0,0 +1,8 @@
+export type RdiInstance = {
+ alias: string
+ url: string
+ version?: string
+ lastConnection?: string
+ username?: string
+ password?: string
+}
diff --git a/tests/playwright/yarn.lock b/tests/playwright/yarn.lock
new file mode 100644
index 0000000000..372c760a93
--- /dev/null
+++ b/tests/playwright/yarn.lock
@@ -0,0 +1,1978 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@ampproject/remapping@^2.2.0":
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
+ integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.5"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@babel/code-frame@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
+ integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.27.1"
+ js-tokens "^4.0.0"
+ picocolors "^1.1.1"
+
+"@babel/compat-data@^7.27.2":
+ version "7.28.0"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.0.tgz#9fc6fd58c2a6a15243cd13983224968392070790"
+ integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==
+
+"@babel/core@^7.23.9":
+ version "7.28.0"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.0.tgz#55dad808d5bf3445a108eefc88ea3fdf034749a4"
+ integrity sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==
+ dependencies:
+ "@ampproject/remapping" "^2.2.0"
+ "@babel/code-frame" "^7.27.1"
+ "@babel/generator" "^7.28.0"
+ "@babel/helper-compilation-targets" "^7.27.2"
+ "@babel/helper-module-transforms" "^7.27.3"
+ "@babel/helpers" "^7.27.6"
+ "@babel/parser" "^7.28.0"
+ "@babel/template" "^7.27.2"
+ "@babel/traverse" "^7.28.0"
+ "@babel/types" "^7.28.0"
+ convert-source-map "^2.0.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.2.3"
+ semver "^6.3.1"
+
+"@babel/generator@^7.28.0":
+ version "7.28.0"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.0.tgz#9cc2f7bd6eb054d77dc66c2664148a0c5118acd2"
+ integrity sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==
+ dependencies:
+ "@babel/parser" "^7.28.0"
+ "@babel/types" "^7.28.0"
+ "@jridgewell/gen-mapping" "^0.3.12"
+ "@jridgewell/trace-mapping" "^0.3.28"
+ jsesc "^3.0.2"
+
+"@babel/helper-compilation-targets@^7.27.2":
+ version "7.27.2"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d"
+ integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==
+ dependencies:
+ "@babel/compat-data" "^7.27.2"
+ "@babel/helper-validator-option" "^7.27.1"
+ browserslist "^4.24.0"
+ lru-cache "^5.1.1"
+ semver "^6.3.1"
+
+"@babel/helper-globals@^7.28.0":
+ version "7.28.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674"
+ integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
+
+"@babel/helper-module-imports@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204"
+ integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==
+ dependencies:
+ "@babel/traverse" "^7.27.1"
+ "@babel/types" "^7.27.1"
+
+"@babel/helper-module-transforms@^7.27.3":
+ version "7.27.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz#db0bbcfba5802f9ef7870705a7ef8788508ede02"
+ integrity sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==
+ dependencies:
+ "@babel/helper-module-imports" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.27.1"
+ "@babel/traverse" "^7.27.3"
+
+"@babel/helper-string-parser@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
+ integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
+
+"@babel/helper-validator-identifier@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
+ integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
+
+"@babel/helper-validator-option@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f"
+ integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==
+
+"@babel/helpers@^7.27.6":
+ version "7.27.6"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.6.tgz#6456fed15b2cb669d2d1fabe84b66b34991d812c"
+ integrity sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==
+ dependencies:
+ "@babel/template" "^7.27.2"
+ "@babel/types" "^7.27.6"
+
+"@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.0":
+ version "7.28.0"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.0.tgz#979829fbab51a29e13901e5a80713dbcb840825e"
+ integrity sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==
+ dependencies:
+ "@babel/types" "^7.28.0"
+
+"@babel/template@^7.27.2":
+ version "7.27.2"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
+ integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==
+ dependencies:
+ "@babel/code-frame" "^7.27.1"
+ "@babel/parser" "^7.27.2"
+ "@babel/types" "^7.27.1"
+
+"@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3", "@babel/traverse@^7.28.0":
+ version "7.28.0"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.0.tgz#518aa113359b062042379e333db18380b537e34b"
+ integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==
+ dependencies:
+ "@babel/code-frame" "^7.27.1"
+ "@babel/generator" "^7.28.0"
+ "@babel/helper-globals" "^7.28.0"
+ "@babel/parser" "^7.28.0"
+ "@babel/template" "^7.27.2"
+ "@babel/types" "^7.28.0"
+ debug "^4.3.1"
+
+"@babel/types@^7.27.1", "@babel/types@^7.27.6", "@babel/types@^7.28.0":
+ version "7.28.1"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.1.tgz#2aaf3c10b31ba03a77ac84f52b3912a0edef4cf9"
+ integrity sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==
+ dependencies:
+ "@babel/helper-string-parser" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.27.1"
+
+"@faker-js/faker@^9.6.0":
+ version "9.6.0"
+ resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.6.0.tgz#64235d20330b142eef3d1d1638ba56c083b4bf1d"
+ integrity sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw==
+
+"@gar/promisify@^1.0.1":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
+ integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
+
+"@istanbuljs/load-nyc-config@^1.0.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
+ integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==
+ dependencies:
+ camelcase "^5.3.1"
+ find-up "^4.1.0"
+ get-package-type "^0.1.0"
+ js-yaml "^3.13.1"
+ resolve-from "^5.0.0"
+
+"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3":
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
+ integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
+
+"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5":
+ version "0.3.12"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz#2234ce26c62889f03db3d7fea43c1932ab3e927b"
+ integrity sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.0"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+ integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
+"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz#7358043433b2e5da569aa02cbc4c121da3af27d7"
+ integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==
+
+"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28":
+ version "0.3.29"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz#a58d31eaadaf92c6695680b2e1d464a9b8fbf7fc"
+ integrity sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@npmcli/fs@^1.0.0":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257"
+ integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==
+ dependencies:
+ "@gar/promisify" "^1.0.1"
+ semver "^7.3.5"
+
+"@npmcli/move-file@^1.0.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
+ integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==
+ dependencies:
+ mkdirp "^1.0.4"
+ rimraf "^3.0.2"
+
+"@playwright/test@^1.52.0":
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.52.0.tgz#267ec595b43a8f4fa5e444ea503689629e91a5b8"
+ integrity sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==
+ dependencies:
+ playwright "1.52.0"
+
+"@tootallnate/once@1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
+ integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
+
+"@types/node@^22.15.29":
+ version "22.15.29"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.29.tgz#c75999124a8224a3f79dd8b6ccfb37d74098f678"
+ integrity sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==
+ dependencies:
+ undici-types "~6.21.0"
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+agent-base@6, agent-base@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
+ integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+ dependencies:
+ debug "4"
+
+agentkeepalive@^4.1.3:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a"
+ integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==
+ dependencies:
+ humanize-ms "^1.2.1"
+
+aggregate-error@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
+ integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
+ dependencies:
+ clean-stack "^2.0.0"
+ indent-string "^4.0.0"
+
+allure-commandline@^2.33.0:
+ version "2.33.0"
+ resolved "https://registry.yarnpkg.com/allure-commandline/-/allure-commandline-2.33.0.tgz#140560c615ea904ff34c061c4c4b6d43858b2b68"
+ integrity sha512-oGMW1Zaqd9SqYJHUqeET1AP363guQkswnCKD+6jSX9YCK8BbttSqZJy9PeSmJtU16uW3qGB6cvgrvJwKUWG5Ew==
+
+allure-js-commons@3.2.0, allure-js-commons@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/allure-js-commons/-/allure-js-commons-3.2.0.tgz#064503cec8735564599c90fff5a239c36d016d66"
+ integrity sha512-UXRo3D6/XEIMosro+OldWj8phJ65eSOYaAUlThOpl6nJJ0sGngMpJYog+Z9FmZDo1BZn4edwLs4aAUaTgkz4Cg==
+ dependencies:
+ md5 "^2.3.0"
+
+allure-playwright@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/allure-playwright/-/allure-playwright-3.2.0.tgz#4bbb276c6785ee7a90540e0b2c93d6ccf273caa7"
+ integrity sha512-E9YNqFBXrycMaOs4x5/Tsdl4xN8Ss0yw8XnwcVUzezR3cjlPb5gUdR81G/zQsi+I3mb+UQMS21yORHKTI9W2fw==
+ dependencies:
+ allure-js-commons "3.2.0"
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^4.0.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+append-transform@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-2.0.0.tgz#99d9d29c7b38391e6f428d28ce136551f0b77e12"
+ integrity sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==
+ dependencies:
+ default-require-extensions "^3.0.0"
+
+"aproba@^1.0.3 || ^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
+ integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
+
+archy@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
+ integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==
+
+are-we-there-yet@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd"
+ integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^3.6.0"
+
+argparse@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+ integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+ dependencies:
+ sprintf-js "~1.0.2"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+axios@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.9.0.tgz#25534e3b72b54540077d33046f77e3b8d7081901"
+ integrity sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==
+ dependencies:
+ follow-redirects "^1.15.6"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+base64-js@^1.3.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+bindings@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+ integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+ dependencies:
+ file-uri-to-path "1.0.0"
+
+bl@^4.0.3:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
+ integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
+ dependencies:
+ buffer "^5.5.0"
+ inherits "^2.0.4"
+ readable-stream "^3.4.0"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+browserslist@^4.24.0:
+ version "4.25.1"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.1.tgz#ba9e8e6f298a1d86f829c9b975e07948967bb111"
+ integrity sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==
+ dependencies:
+ caniuse-lite "^1.0.30001726"
+ electron-to-chromium "^1.5.173"
+ node-releases "^2.0.19"
+ update-browserslist-db "^1.1.3"
+
+buffer@^5.5.0:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+ integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.1.13"
+
+cacache@^15.2.0:
+ version "15.3.0"
+ resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb"
+ integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==
+ dependencies:
+ "@npmcli/fs" "^1.0.0"
+ "@npmcli/move-file" "^1.0.1"
+ chownr "^2.0.0"
+ fs-minipass "^2.0.0"
+ glob "^7.1.4"
+ infer-owner "^1.0.4"
+ lru-cache "^6.0.0"
+ minipass "^3.1.1"
+ minipass-collect "^1.0.2"
+ minipass-flush "^1.0.5"
+ minipass-pipeline "^1.2.2"
+ mkdirp "^1.0.3"
+ p-map "^4.0.0"
+ promise-inflight "^1.0.1"
+ rimraf "^3.0.2"
+ ssri "^8.0.1"
+ tar "^6.0.2"
+ unique-filename "^1.1.1"
+
+caching-transform@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-4.0.0.tgz#00d297a4206d71e2163c39eaffa8157ac0651f0f"
+ integrity sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==
+ dependencies:
+ hasha "^5.0.0"
+ make-dir "^3.0.0"
+ package-hash "^4.0.0"
+ write-file-atomic "^3.0.0"
+
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+ integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+
+camelcase@^5.0.0, camelcase@^5.3.1:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+ integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
+caniuse-lite@^1.0.30001726:
+ version "1.0.30001727"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz#22e9706422ad37aa50556af8c10e40e2d93a8b85"
+ integrity sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==
+
+charenc@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
+ integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==
+
+chownr@^1.1.1:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+ integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
+chownr@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
+ integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
+
+clean-stack@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
+ integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+
+cliui@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+ integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^6.2.0"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-support@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
+ integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
+
+combined-stream@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+ integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+console-control-strings@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+ integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==
+
+convert-source-map@^1.7.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
+ integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
+
+convert-source-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
+ integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
+
+cross-env@^7.0.3:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
+ integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
+ dependencies:
+ cross-spawn "^7.0.1"
+
+cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.3, cross-spawn@^7.0.6:
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+ integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
+crypt@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
+ integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==
+
+debug@4, debug@^4.3.3:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
+ integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
+ dependencies:
+ ms "^2.1.3"
+
+debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
+ integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
+ dependencies:
+ ms "^2.1.3"
+
+decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+
+decompress-response@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
+ integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
+ dependencies:
+ mimic-response "^3.1.0"
+
+deep-extend@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+ integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+default-require-extensions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.1.tgz#bfae00feeaeada68c2ae256c62540f60b80625bd"
+ integrity sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==
+ dependencies:
+ strip-bom "^4.0.0"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+ integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
+
+detect-libc@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
+ integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
+
+dotenv-cli@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-8.0.0.tgz#cea1519f5a06c7372a1428fca4605fcf3d50e1cf"
+ integrity sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==
+ dependencies:
+ cross-spawn "^7.0.6"
+ dotenv "^16.3.0"
+ dotenv-expand "^10.0.0"
+ minimist "^1.2.6"
+
+dotenv-expand@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37"
+ integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==
+
+dotenv@^16.3.0, dotenv@^16.4.7:
+ version "16.4.7"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26"
+ integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==
+
+dunder-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+ integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+ dependencies:
+ call-bind-apply-helpers "^1.0.1"
+ es-errors "^1.3.0"
+ gopd "^1.2.0"
+
+electron-to-chromium@^1.5.173:
+ version "1.5.182"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz#4ab73104f893938acb3ab9c28d7bec170c116b3e"
+ integrity sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+encoding@^0.1.12:
+ version "0.1.13"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
+ integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
+ dependencies:
+ iconv-lite "^0.6.2"
+
+end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+ integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+ dependencies:
+ once "^1.4.0"
+
+env-paths@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
+ integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
+
+err-code@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
+ integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
+
+es-define-property@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+ integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
+ dependencies:
+ es-errors "^1.3.0"
+
+es-set-tostringtag@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
+ integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
+ dependencies:
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.6"
+ has-tostringtag "^1.0.2"
+ hasown "^2.0.2"
+
+es6-error@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
+ integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
+
+escalade@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
+ integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
+
+esprima@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+expand-template@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
+ integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
+
+file-uri-to-path@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+ integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
+find-cache-dir@^3.2.0:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
+ integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^3.0.2"
+ pkg-dir "^4.1.0"
+
+find-up@^4.0.0, find-up@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+follow-redirects@^1.15.6:
+ version "1.15.9"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
+ integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
+
+foreground-child@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53"
+ integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==
+ dependencies:
+ cross-spawn "^7.0.0"
+ signal-exit "^3.0.2"
+
+foreground-child@^3.3.0:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
+ integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
+ dependencies:
+ cross-spawn "^7.0.6"
+ signal-exit "^4.0.1"
+
+form-data@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.3.tgz#608b1b3f3e28be0fccf5901fc85fb3641e5cf0ae"
+ integrity sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ es-set-tostringtag "^2.1.0"
+ hasown "^2.0.2"
+ mime-types "^2.1.12"
+
+fromentries@^1.2.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a"
+ integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==
+
+fs-constants@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+ integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
+fs-extra@^11.3.0:
+ version "11.3.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d"
+ integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^6.0.1"
+ universalify "^2.0.0"
+
+fs-minipass@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
+ integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
+ dependencies:
+ minipass "^3.0.0"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+gauge@^4.0.3:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce"
+ integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==
+ dependencies:
+ aproba "^1.0.3 || ^2.0.0"
+ color-support "^1.1.3"
+ console-control-strings "^1.1.0"
+ has-unicode "^2.0.1"
+ signal-exit "^3.0.7"
+ string-width "^4.2.3"
+ strip-ansi "^6.0.1"
+ wide-align "^1.1.5"
+
+gensync@^1.0.0-beta.2:
+ version "1.0.0-beta.2"
+ resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+ integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
+get-caller-file@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-intrinsic@^1.2.6:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+ integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
+ dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ es-define-property "^1.0.1"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.1.1"
+ function-bind "^1.1.2"
+ get-proto "^1.0.1"
+ gopd "^1.2.0"
+ has-symbols "^1.1.0"
+ hasown "^2.0.2"
+ math-intrinsics "^1.1.0"
+
+get-package-type@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
+ integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
+
+get-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+ integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+ dependencies:
+ dunder-proto "^1.0.1"
+ es-object-atoms "^1.0.0"
+
+github-from-package@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
+ integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
+
+glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+ integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.1.1"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+gopd@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+ integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
+
+graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+ integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+has-tostringtag@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+ integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+ dependencies:
+ has-symbols "^1.0.3"
+
+has-unicode@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+ integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==
+
+hasha@^5.0.0:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1"
+ integrity sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==
+ dependencies:
+ is-stream "^2.0.0"
+ type-fest "^0.8.0"
+
+hasown@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
+html-escaper@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
+ integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+
+http-cache-semantics@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
+ integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
+
+http-proxy-agent@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
+ integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==
+ dependencies:
+ "@tootallnate/once" "1"
+ agent-base "6"
+ debug "4"
+
+https-proxy-agent@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
+ integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
+ dependencies:
+ agent-base "6"
+ debug "4"
+
+humanize-ms@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
+ integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
+ dependencies:
+ ms "^2.0.0"
+
+iconv-lite@^0.6.2:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
+ieee754@^1.1.13:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+ integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+ integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
+
+indent-string@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
+ integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+
+infer-owner@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
+ integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.3, inherits@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ini@~1.3.0:
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+ integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+
+ip-address@^9.0.5:
+ version "9.0.5"
+ resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a"
+ integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==
+ dependencies:
+ jsbn "1.1.0"
+ sprintf-js "^1.1.3"
+
+is-buffer@~1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-lambda@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
+ integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==
+
+is-stream@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+ integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
+
+is-typedarray@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+ integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
+
+is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756"
+ integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==
+
+istanbul-lib-hook@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz#8f84c9434888cc6b1d0a9d7092a76d239ebf0cc6"
+ integrity sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==
+ dependencies:
+ append-transform "^2.0.0"
+
+istanbul-lib-instrument@^6.0.2:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765"
+ integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==
+ dependencies:
+ "@babel/core" "^7.23.9"
+ "@babel/parser" "^7.23.9"
+ "@istanbuljs/schema" "^0.1.3"
+ istanbul-lib-coverage "^3.2.0"
+ semver "^7.5.4"
+
+istanbul-lib-processinfo@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz#366d454cd0dcb7eb6e0e419378e60072c8626169"
+ integrity sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==
+ dependencies:
+ archy "^1.0.0"
+ cross-spawn "^7.0.3"
+ istanbul-lib-coverage "^3.2.0"
+ p-map "^3.0.0"
+ rimraf "^3.0.0"
+ uuid "^8.3.2"
+
+istanbul-lib-report@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d"
+ integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==
+ dependencies:
+ istanbul-lib-coverage "^3.0.0"
+ make-dir "^4.0.0"
+ supports-color "^7.1.0"
+
+istanbul-lib-source-maps@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551"
+ integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==
+ dependencies:
+ debug "^4.1.1"
+ istanbul-lib-coverage "^3.0.0"
+ source-map "^0.6.1"
+
+istanbul-reports@^3.0.2:
+ version "3.1.7"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b"
+ integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==
+ dependencies:
+ html-escaper "^2.0.0"
+ istanbul-lib-report "^3.0.0"
+
+js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^3.13.1:
+ version "3.14.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
+ integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
+ integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==
+
+jsesc@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
+ integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
+
+json5@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+ integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+jsonfile@^6.0.1:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
+ integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
+ dependencies:
+ universalify "^2.0.0"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+lodash.flattendeep@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+ integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+lru-cache@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+ integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+ dependencies:
+ yallist "^4.0.0"
+
+make-dir@^3.0.0, make-dir@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
+ integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
+ dependencies:
+ semver "^6.0.0"
+
+make-dir@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
+ integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==
+ dependencies:
+ semver "^7.5.3"
+
+make-fetch-happen@^9.1.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968"
+ integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==
+ dependencies:
+ agentkeepalive "^4.1.3"
+ cacache "^15.2.0"
+ http-cache-semantics "^4.1.0"
+ http-proxy-agent "^4.0.1"
+ https-proxy-agent "^5.0.0"
+ is-lambda "^1.0.1"
+ lru-cache "^6.0.0"
+ minipass "^3.1.3"
+ minipass-collect "^1.0.2"
+ minipass-fetch "^1.3.2"
+ minipass-flush "^1.0.5"
+ minipass-pipeline "^1.2.4"
+ negotiator "^0.6.2"
+ promise-retry "^2.0.1"
+ socks-proxy-agent "^6.0.0"
+ ssri "^8.0.0"
+
+math-intrinsics@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+ integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+
+md5@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
+ integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==
+ dependencies:
+ charenc "0.0.2"
+ crypt "0.0.2"
+ is-buffer "~1.1.6"
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+ version "2.1.35"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+mimic-response@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
+ integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
+
+minimatch@^3.0.4, minimatch@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+minipass-collect@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
+ integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
+ dependencies:
+ minipass "^3.0.0"
+
+minipass-fetch@^1.3.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6"
+ integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==
+ dependencies:
+ minipass "^3.1.0"
+ minipass-sized "^1.0.3"
+ minizlib "^2.0.0"
+ optionalDependencies:
+ encoding "^0.1.12"
+
+minipass-flush@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
+ integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
+ dependencies:
+ minipass "^3.0.0"
+
+minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
+ integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
+ dependencies:
+ minipass "^3.0.0"
+
+minipass-sized@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70"
+ integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==
+ dependencies:
+ minipass "^3.0.0"
+
+minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3:
+ version "3.3.6"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a"
+ integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==
+ dependencies:
+ yallist "^4.0.0"
+
+minipass@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
+ integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
+
+minizlib@^2.0.0, minizlib@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
+ integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
+ dependencies:
+ minipass "^3.0.0"
+ yallist "^4.0.0"
+
+mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
+ integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
+
+mkdirp@^1.0.3, mkdirp@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+ integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
+ms@^2.0.0, ms@^2.1.3:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+napi-build-utils@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e"
+ integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==
+
+negotiator@^0.6.2:
+ version "0.6.4"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
+ integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
+
+node-abi@^3.3.0:
+ version "3.74.0"
+ resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.74.0.tgz#5bfb4424264eaeb91432d2adb9da23c63a301ed0"
+ integrity sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==
+ dependencies:
+ semver "^7.3.5"
+
+node-addon-api@^7.0.0:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
+ integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
+
+node-color-log@^12.0.1:
+ version "12.0.1"
+ resolved "https://registry.yarnpkg.com/node-color-log/-/node-color-log-12.0.1.tgz#47d982e3cb6aa90c2936ca38cd910ef82076c6f5"
+ integrity sha512-fhbWy00HXAVucPHoji9KNZRtXHcDKuMoVJ3QA+vaMEcAyK6psmJAf5TF9t2SmkybuHz0jre+jgUDyXcFmpgSNg==
+
+node-gyp@8.x:
+ version "8.4.1"
+ resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
+ integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==
+ dependencies:
+ env-paths "^2.2.0"
+ glob "^7.1.4"
+ graceful-fs "^4.2.6"
+ make-fetch-happen "^9.1.0"
+ nopt "^5.0.0"
+ npmlog "^6.0.0"
+ rimraf "^3.0.2"
+ semver "^7.3.5"
+ tar "^6.1.2"
+ which "^2.0.2"
+
+node-preload@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301"
+ integrity sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==
+ dependencies:
+ process-on-spawn "^1.0.0"
+
+node-releases@^2.0.19:
+ version "2.0.19"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
+ integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
+
+nopt@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
+ integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
+ dependencies:
+ abbrev "1"
+
+npmlog@^6.0.0:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830"
+ integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==
+ dependencies:
+ are-we-there-yet "^3.0.0"
+ console-control-strings "^1.1.0"
+ gauge "^4.0.3"
+ set-blocking "^2.0.0"
+
+nyc@^17.1.0:
+ version "17.1.0"
+ resolved "https://registry.yarnpkg.com/nyc/-/nyc-17.1.0.tgz#b6349a401a62ffeb912bd38ea9a018839fdb6eb1"
+ integrity sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==
+ dependencies:
+ "@istanbuljs/load-nyc-config" "^1.0.0"
+ "@istanbuljs/schema" "^0.1.2"
+ caching-transform "^4.0.0"
+ convert-source-map "^1.7.0"
+ decamelize "^1.2.0"
+ find-cache-dir "^3.2.0"
+ find-up "^4.1.0"
+ foreground-child "^3.3.0"
+ get-package-type "^0.1.0"
+ glob "^7.1.6"
+ istanbul-lib-coverage "^3.0.0"
+ istanbul-lib-hook "^3.0.0"
+ istanbul-lib-instrument "^6.0.2"
+ istanbul-lib-processinfo "^2.0.2"
+ istanbul-lib-report "^3.0.0"
+ istanbul-lib-source-maps "^4.0.0"
+ istanbul-reports "^3.0.2"
+ make-dir "^3.0.0"
+ node-preload "^0.2.1"
+ p-map "^3.0.0"
+ process-on-spawn "^1.0.0"
+ resolve-from "^5.0.0"
+ rimraf "^3.0.0"
+ signal-exit "^3.0.2"
+ spawn-wrap "^2.0.0"
+ test-exclude "^6.0.0"
+ yargs "^15.0.2"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-map@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d"
+ integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==
+ dependencies:
+ aggregate-error "^3.0.0"
+
+p-map@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
+ integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
+ dependencies:
+ aggregate-error "^3.0.0"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+package-hash@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-4.0.0.tgz#3537f654665ec3cc38827387fc904c163c54f506"
+ integrity sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==
+ dependencies:
+ graceful-fs "^4.1.15"
+ hasha "^5.0.0"
+ lodash.flattendeep "^4.4.0"
+ release-zalgo "^1.0.0"
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+picocolors@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+ integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+pkg-dir@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+ integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+ dependencies:
+ find-up "^4.0.0"
+
+playwright-core@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.52.0.tgz#238f1f0c3edd4ebba0434ce3f4401900319a3dca"
+ integrity sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==
+
+playwright@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.52.0.tgz#26cb9a63346651e1c54c8805acfd85683173d4bd"
+ integrity sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==
+ dependencies:
+ playwright-core "1.52.0"
+ optionalDependencies:
+ fsevents "2.3.2"
+
+prebuild-install@^7.1.1:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec"
+ integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==
+ dependencies:
+ detect-libc "^2.0.0"
+ expand-template "^2.0.3"
+ github-from-package "0.0.0"
+ minimist "^1.2.3"
+ mkdirp-classic "^0.5.3"
+ napi-build-utils "^2.0.0"
+ node-abi "^3.3.0"
+ pump "^3.0.0"
+ rc "^1.2.7"
+ simple-get "^4.0.0"
+ tar-fs "^2.0.0"
+ tunnel-agent "^0.6.0"
+
+process-on-spawn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/process-on-spawn/-/process-on-spawn-1.1.0.tgz#9d5999ba87b3bf0a8acb05322d69f2f5aa4fb763"
+ integrity sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==
+ dependencies:
+ fromentries "^1.2.0"
+
+promise-inflight@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+ integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==
+
+promise-retry@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
+ integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==
+ dependencies:
+ err-code "^2.0.2"
+ retry "^0.12.0"
+
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+pump@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8"
+ integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+rc@^1.2.7:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+ integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
+ integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+release-zalgo@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730"
+ integrity sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==
+ dependencies:
+ es6-error "^4.0.1"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+require-main-filename@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+ integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
+resolve-from@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+ integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+retry@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
+ integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==
+
+rimraf@^3.0.0, rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
+safe-buffer@^5.0.1, safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+"safer-buffer@>= 2.1.2 < 3.0.0":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+semver@^6.0.0, semver@^6.3.1:
+ version "6.3.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
+ integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
+
+semver@^7.3.5:
+ version "7.7.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
+ integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
+
+semver@^7.5.3, semver@^7.5.4:
+ version "7.7.2"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+ integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
+
+set-blocking@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+signal-exit@^3.0.2, signal-exit@^3.0.7:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+ integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+signal-exit@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
+ integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
+
+simple-concat@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
+ integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
+
+simple-get@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
+ integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
+ dependencies:
+ decompress-response "^6.0.0"
+ once "^1.3.1"
+ simple-concat "^1.0.0"
+
+smart-buffer@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
+ integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
+
+socks-proxy-agent@^6.0.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce"
+ integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==
+ dependencies:
+ agent-base "^6.0.2"
+ debug "^4.3.3"
+ socks "^2.6.2"
+
+socks@^2.6.2:
+ version "2.8.4"
+ resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.4.tgz#07109755cdd4da03269bda4725baa061ab56d5cc"
+ integrity sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==
+ dependencies:
+ ip-address "^9.0.5"
+ smart-buffer "^4.2.0"
+
+source-map@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spawn-wrap@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-2.0.0.tgz#103685b8b8f9b79771318827aa78650a610d457e"
+ integrity sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==
+ dependencies:
+ foreground-child "^2.0.0"
+ is-windows "^1.0.2"
+ make-dir "^3.0.0"
+ rimraf "^3.0.0"
+ signal-exit "^3.0.2"
+ which "^2.0.1"
+
+sprintf-js@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a"
+ integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+ integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
+
+sqlite3@^5.1.7:
+ version "5.1.7"
+ resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.7.tgz#59ca1053c1ab38647396586edad019b1551041b7"
+ integrity sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==
+ dependencies:
+ bindings "^1.5.0"
+ node-addon-api "^7.0.0"
+ prebuild-install "^7.1.1"
+ tar "^6.1.11"
+ optionalDependencies:
+ node-gyp "8.x"
+
+ssri@^8.0.0, ssri@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
+ integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
+ dependencies:
+ minipass "^3.1.1"
+
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-bom@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
+ integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+ integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+tar-fs@^2.0.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.2.tgz#425f154f3404cb16cb8ff6e671d45ab2ed9596c5"
+ integrity sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==
+ dependencies:
+ chownr "^1.1.1"
+ mkdirp-classic "^0.5.2"
+ pump "^3.0.0"
+ tar-stream "^2.1.4"
+
+tar-stream@^2.1.4:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
+ integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
+ dependencies:
+ bl "^4.0.3"
+ end-of-stream "^1.4.1"
+ fs-constants "^1.0.0"
+ inherits "^2.0.3"
+ readable-stream "^3.1.1"
+
+tar@^6.0.2, tar@^6.1.11, tar@^6.1.2:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
+ integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
+ dependencies:
+ chownr "^2.0.0"
+ fs-minipass "^2.0.0"
+ minipass "^5.0.0"
+ minizlib "^2.1.1"
+ mkdirp "^1.0.3"
+ yallist "^4.0.0"
+
+test-exclude@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
+ integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
+ dependencies:
+ "@istanbuljs/schema" "^0.1.2"
+ glob "^7.1.4"
+ minimatch "^3.0.4"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
+ dependencies:
+ safe-buffer "^5.0.1"
+
+type-fest@^0.8.0:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+ integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
+typedarray-to-buffer@^3.1.5:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
+ integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
+ dependencies:
+ is-typedarray "^1.0.0"
+
+undici-types@~6.21.0:
+ version "6.21.0"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
+ integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
+
+unique-filename@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
+ integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+ dependencies:
+ unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
+ integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
+ dependencies:
+ imurmurhash "^0.1.4"
+
+universalify@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
+ integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
+
+update-browserslist-db@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
+ integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
+ dependencies:
+ escalade "^3.2.0"
+ picocolors "^1.1.1"
+
+util-deprecate@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+uuid@^8.3.2:
+ version "8.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+ integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
+which-module@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
+ integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
+
+which@^2.0.1, which@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
+ integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
+ dependencies:
+ string-width "^1.0.2 || 2 || 3 || 4"
+
+wrap-ansi@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+ integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+write-file-atomic@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
+ integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
+ dependencies:
+ imurmurhash "^0.1.4"
+ is-typedarray "^1.0.0"
+ signal-exit "^3.0.2"
+ typedarray-to-buffer "^3.1.5"
+
+y18n@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+ integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
+
+yallist@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yargs-parser@^18.1.2:
+ version "18.1.3"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+ integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs@^15.0.2:
+ version "15.4.1"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+ integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
+ dependencies:
+ cliui "^6.0.0"
+ decamelize "^1.2.0"
+ find-up "^4.1.0"
+ get-caller-file "^2.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^2.0.0"
+ set-blocking "^2.0.0"
+ string-width "^4.2.0"
+ which-module "^2.0.0"
+ y18n "^4.0.0"
+ yargs-parser "^18.1.2"
diff --git a/yarn.lock b/yarn.lock
index fec175a88b..a37c932b01 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1465,7 +1465,7 @@
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
-"@istanbuljs/load-nyc-config@^1.0.0":
+"@istanbuljs/load-nyc-config@^1.0.0", "@istanbuljs/load-nyc-config@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==
@@ -3600,7 +3600,7 @@ acorn-import-attributes@^1.9.5:
resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==
-acorn-jsx@^5.3.1:
+acorn-jsx@^5.3.1, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
@@ -3615,10 +3615,10 @@ acorn@^7.4.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
-acorn@^8.0.4, acorn@^8.0.5, acorn@^8.1.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2:
- version "8.10.0"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
- integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
+acorn@^8.0.4, acorn@^8.0.5, acorn@^8.1.0, acorn@^8.15.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2:
+ version "8.15.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
+ integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
agent-base@6, agent-base@^6.0.2:
version "6.0.2"
@@ -6543,6 +6543,11 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc"
integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==
+eslint-visitor-keys@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
+ integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
+
eslint@^7.5.0:
version "7.32.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
@@ -6589,6 +6594,15 @@ eslint@^7.5.0:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
+espree@^10.3.0:
+ version "10.4.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837"
+ integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
+ dependencies:
+ acorn "^8.15.0"
+ acorn-jsx "^5.3.2"
+ eslint-visitor-keys "^4.2.1"
+
espree@^7.3.0, espree@^7.3.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
@@ -7155,10 +7169,10 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
-glob@^10.3.10:
- version "10.4.2"
- resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5"
- integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==
+glob@^10.3.10, glob@^10.4.1:
+ version "10.4.5"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
+ integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
dependencies:
foreground-child "^3.1.0"
jackspeak "^3.1.2"
@@ -8321,7 +8335,7 @@ istanbul-lib-instrument@^5.0.4:
istanbul-lib-coverage "^3.2.0"
semver "^6.3.0"
-istanbul-lib-instrument@^6.0.0:
+istanbul-lib-instrument@^6.0.0, istanbul-lib-instrument@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765"
integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==
@@ -10686,7 +10700,7 @@ pickleparser@^0.2.1:
resolved "https://registry.yarnpkg.com/pickleparser/-/pickleparser-0.2.1.tgz#7a03f1e9204e91ec9b8efbd3ba2f1eb5955b994d"
integrity sha512-kMzY3uFYcR6OjOqr7nV2nkaXaBsUEOafu3zgPxeD6s/2ueMfVQH8lrymcDWBPGx0OkVxGMikxQit6jgByXjwBg==
-picocolors@^1.0.0, picocolors@^1.1.0:
+picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
@@ -13061,6 +13075,15 @@ test-exclude@^6.0.0:
glob "^7.1.4"
minimatch "^3.0.4"
+test-exclude@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.1.tgz#20b3ba4906ac20994e275bbcafd68d510264c2a2"
+ integrity sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==
+ dependencies:
+ "@istanbuljs/schema" "^0.1.2"
+ glob "^10.4.1"
+ minimatch "^9.0.4"
+
text-diff@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/text-diff/-/text-diff-1.0.1.tgz#6c105905435e337857375c9d2f6ca63e453ff565"
@@ -13893,6 +13916,18 @@ vite-plugin-electron@^0.28.6:
resolved "https://registry.yarnpkg.com/vite-plugin-electron/-/vite-plugin-electron-0.28.6.tgz#98bcf291179dfbdfef407f881cbb1e1d58249c57"
integrity sha512-DANntooA/XcUQuaOG7tQ0nnWh8iP5yKur2e9GDafjslOPAVZehRyrbi2UEI6rlIhN6hHwcqAjY+/Zz8+thAL5g==
+vite-plugin-istanbul@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/vite-plugin-istanbul/-/vite-plugin-istanbul-7.1.0.tgz#f1abd43d38cb094a1dae9d383b28e66a11141f91"
+ integrity sha512-md0774bPYfSrMbAMMy3Xui2+xqmEVwulCGN2ImGm4E4s+0VfO7TjFyJ4ITFIFyEmBhWoMM0sOOX0Yg0I1SsncQ==
+ dependencies:
+ "@istanbuljs/load-nyc-config" "^1.1.0"
+ espree "^10.3.0"
+ istanbul-lib-instrument "^6.0.3"
+ picocolors "^1.1.1"
+ source-map "^0.7.4"
+ test-exclude "^7.0.1"
+
vite-plugin-react-click-to-component@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/vite-plugin-react-click-to-component/-/vite-plugin-react-click-to-component-3.0.0.tgz#bd22d0210ca245bf00b513ad4f6f28065cc98880"