You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2025/05/13 12:40:42 UTC
(apisix-dashboard) branch master updated: feat: e2e workflow, auth test (#3050)
This is an automated email from the ASF dual-hosted git repository.
juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 8cdd4f424 feat: e2e workflow, auth test (#3050)
8cdd4f424 is described below
commit 8cdd4f4241b5a90ae6fc03c7720170e2ce63cb8a
Author: YYYoung <is...@outlook.com>
AuthorDate: Tue May 13 20:40:36 2025 +0800
feat: e2e workflow, auth test (#3050)
---
.devcontainer/devcontainer.json | 3 +-
.devcontainer/docker-compose.override.yml | 10 ++
.devcontainer/docker-compose.yml | 41 +----
.github/workflows/e2e.yml | 94 +++++++++++
.gitignore | 8 +
e2e/auth.spec.ts | 67 ++++++++
.../apisix_conf.yml => e2e/server/Dockerfile | 42 +++--
{.devcontainer => e2e/server}/apisix_conf.yml | 0
.../server/docker-compose.common.yml | 49 +++---
.../server/docker-compose.yml | 54 ++++---
.../apisix_conf.yml => e2e/server/nginx.conf | 38 ++---
src/components/form/Btn.tsx => e2e/utils/common.ts | 19 ++-
src/components/form/Btn.tsx => e2e/utils/env.ts | 25 ++-
e2e/utils/test.ts | 70 +++++++++
eslint.config.js | 69 +++++++-
package.json | 16 +-
playwright.config.ts | 60 +++++++
pnpm-lock.yaml | 174 +++++++++++++++++++++
src/components/form/Btn.tsx | 10 +-
tsconfig.app.json | 5 +-
tsconfig.e2e.json | 27 ++++
tsconfig.json | 7 +-
22 files changed, 731 insertions(+), 157 deletions(-)
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index ead684d11..22c663e3f 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -50,7 +50,8 @@
"lokalise.i18n-ally",
"formulahendry.auto-close-tag",
"formulahendry.auto-rename-tag",
- "github.vscode-pull-request-github"
+ "github.vscode-pull-request-github",
+ "redhat.vscode-yaml"
]
}
}
diff --git a/.devcontainer/docker-compose.override.yml b/.devcontainer/docker-compose.override.yml
new file mode 100644
index 000000000..9a546f623
--- /dev/null
+++ b/.devcontainer/docker-compose.override.yml
@@ -0,0 +1,10 @@
+services:
+ apisix:
+ ports:
+ - '9180:9180/tcp'
+ - '9080:9080/tcp'
+ - '9091:9091/tcp'
+ - '9443:9443/tcp'
+ etcd:
+ ports:
+ - '2379:2379/tcp'
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 7167b8aa0..3180bb90e 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -1,3 +1,7 @@
+include:
+ - path:
+ - ../e2e/server/docker-compose.common.yml
+ - docker-compose.override.yml
services:
apisix-dashboard:
build:
@@ -11,40 +15,3 @@ services:
ports:
- '5173:5173'
- '5174:5174'
-
- apisix:
- image: 'apache/apisix:3.12.0-debian'
- restart: always
- volumes:
- - ./apisix_conf.yml:/usr/local/apisix/conf/config.yaml:ro
- depends_on:
- - etcd
- ports:
- - '9180:9180/tcp'
- - '9080:9080/tcp'
- - '9091:9091/tcp'
- - '9443:9443/tcp'
- networks:
- - apisix
-
- etcd:
- image: bitnami/etcd:3.5
- restart: always
- volumes:
- - etcd_data:/bitnami/etcd
- environment:
- ETCD_ENABLE_V2: 'true'
- ALLOW_NONE_AUTHENTICATION: 'yes'
- ETCD_ADVERTISE_CLIENT_URLS: 'http://etcd:2379'
- ETCD_LISTEN_CLIENT_URLS: 'http://0.0.0.0:2379'
- ports:
- - '2379:2379/tcp'
- networks:
- - apisix
-
-networks:
- apisix:
- driver: bridge
-
-volumes:
- etcd_data:
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 000000000..4ccef7fac
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,94 @@
+name: Frontend e2e test
+
+on:
+ push:
+ branches:
+ - "**"
+ pull_request:
+ types: [labeled, synchronize]
+ branches:
+ - "**"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ if: ${{ contains(github.event.pull_request.labels.*.name, 'e2e-test') }}
+ timeout-minutes: 40
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Get PR Labels
+ id: get-labels
+ uses: actions/github-script@v6
+ with:
+ script: |
+ const labels = context.payload.pull_request.labels.map(label => label.name);
+ core.setOutput("labels", labels.join(","));
+ - name: Set image label as environment variable
+ run: |
+ export LABELS=${{ steps.get-labels.outputs.labels }}
+ echo "LABELS=${LABELS}" >> $GITHUB_ENV
+ shell: bash
+
+ # ensure this in https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+ - uses: pnpm/action-setup@v4
+ name: Install pnpm
+ with:
+ run_install: false
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: |
+ pnpm install --frozen-lockfile
+
+ - name: Install Playwright Browsers
+ run: |
+ pnpm exec playwright install --with-deps
+
+ - name: Run e2e server
+ working-directory: ./e2e/server
+ run: |
+ docker compose up -d
+
+ - name: Waiting dashboard service to be healthy
+ working-directory: ./e2e/server
+ run: |
+ E2E_SERVER="dashboard-e2e"
+ TIMEOUT=30
+ timeout $TIMEOUT bash -c '
+ until [ "$(docker inspect --format="{{.State.Health.Status}}" $(docker compose ps -q '$E2E_SERVER'))" = "healthy" ]; do
+ echo "'$E2E_SERVER' is starting..."
+ sleep 5
+ done
+ ' || (echo "$E2E_SERVER not healthy after $TIMEOUT seconds" && exit 1)
+
+ - name: Run e2e tests
+ run: |
+ pnpm e2e
+
+ - uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() }}
+ with:
+ name: test-results
+ path: apps/site-e2e/test-results/
+ retention-days: 7
+
+ - uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() }}
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 7
+
+ - name: Print Components Logs
+ if: failure()
+ run: |
+ docker ps --format '{{.Names}}' | xargs -I{} bash -c "echo ================= {} ==================== && docker logs {} && echo ================= {} ===================="
diff --git a/.gitignore b/.gitignore
index 15c6f2f74..3e3417ee5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,11 @@ dist-ssr
*.sw?
/.pnpm-store
+
+# Playwright
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+
+.eslintcache
diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts
new file mode 100644
index 000000000..58cd14e4c
--- /dev/null
+++ b/e2e/auth.spec.ts
@@ -0,0 +1,67 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect } from '@playwright/test';
+import { test } from '@utils/test';
+
+import { getAPISIXConf } from './utils/common';
+
+// use empty storage state to avoid auth
+test.use({ storageState: { cookies: [], origins: [] } });
+
+test('can auth with admin key', { tag: '@auth' }, async ({ page }) => {
+ const settingsModal = page.getByRole('dialog', { name: 'Settings' });
+ const adminKeyInput = page.getByRole('textbox', { name: 'Admin Key' });
+ const failedMsg = page.getByText('failed to check token');
+
+ const checkSettingsModal = async () => {
+ await expect(failedMsg).toBeVisible();
+ await expect(settingsModal).toBeVisible();
+ await expect(page.getByText('UI Commit SHA')).toBeVisible();
+ };
+
+ await test.step('fill wrong admin key, settings modal always be visible', async () => {
+ await checkSettingsModal();
+
+ await expect(adminKeyInput).toBeEmpty();
+ await adminKeyInput.fill('wrong-admin-key');
+ await page
+ .getByRole('dialog', { name: 'Settings' })
+ .getByRole('button')
+ .click();
+
+ await page.reload();
+ await expect(failedMsg).toBeVisible();
+ await expect(settingsModal).toBeVisible();
+ });
+
+ await test.step('fill correct admin key, settings modal can be hidden', async () => {
+ await page.reload();
+ await checkSettingsModal();
+
+ const { adminKey } = await getAPISIXConf();
+ await adminKeyInput.clear();
+ await adminKeyInput.fill(adminKey);
+ await page
+ .getByRole('dialog', { name: 'Settings' })
+ .getByRole('button')
+ .click();
+
+ await page.reload();
+ await expect(failedMsg).toBeHidden();
+ });
+});
diff --git a/.devcontainer/apisix_conf.yml b/e2e/server/Dockerfile
similarity index 51%
copy from .devcontainer/apisix_conf.yml
copy to e2e/server/Dockerfile
index 94532cf57..3d675a471 100644
--- a/.devcontainer/apisix_conf.yml
+++ b/e2e/server/Dockerfile
@@ -15,28 +15,26 @@
# limitations under the License.
#
-apisix:
- node_listen: 9080 # APISIX listening port
- enable_ipv6: false
- proxy_mode: http&stream
- stream_proxy:
- tcp:
- - 9100
- udp:
- - 9200
+FROM node:22-alpine AS base
-deployment:
- admin:
- allow_admin: # https://nginx.org/en/docs/http/ngx_http_access_module.html#allow
- - 0.0.0.0/0 # We need to restrict ip access rules for security. 0.0.0.0/0 is for test.
+# install deps
+FROM base AS deps
+WORKDIR /app
+COPY package.json pnpm-lock.yaml* ./
+RUN \
+ if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install; \
+ else echo 'Lockfile not found.' && exit 1; \
+ fi
- admin_key:
- - name: "admin"
- key: edd1c9f034335f136f87ad84b625c8f1
- role: admin # admin: manage all configuration data
+# build ui
+FROM base AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+# we need git to get commit sha
+RUN apk add --no-cache git
+RUN corepack enable pnpm && pnpm build
- etcd:
- host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster.
- - "http://etcd:2379" # multiple etcd address
- prefix: "/apisix" # apisix configurations prefix
- timeout: 30 # 30 seconds
+# serve ui
+FROM nginx:1.28.0-alpine-slim AS runner
+COPY --from=builder /app/dist /usr/share/nginx/html
diff --git a/.devcontainer/apisix_conf.yml b/e2e/server/apisix_conf.yml
similarity index 100%
copy from .devcontainer/apisix_conf.yml
copy to e2e/server/apisix_conf.yml
diff --git a/.devcontainer/apisix_conf.yml b/e2e/server/docker-compose.common.yml
similarity index 52%
copy from .devcontainer/apisix_conf.yml
copy to e2e/server/docker-compose.common.yml
index 94532cf57..d165416aa 100644
--- a/.devcontainer/apisix_conf.yml
+++ b/e2e/server/docker-compose.common.yml
@@ -15,28 +15,33 @@
# limitations under the License.
#
-apisix:
- node_listen: 9080 # APISIX listening port
- enable_ipv6: false
- proxy_mode: http&stream
- stream_proxy:
- tcp:
- - 9100
- udp:
- - 9200
+services:
+ apisix:
+ image: 'apache/apisix:3.12.0-debian'
+ restart: always
+ volumes:
+ - ./apisix_conf.yml:/usr/local/apisix/conf/config.yaml:ro
+ depends_on:
+ - etcd
+ networks:
+ - apisix
-deployment:
- admin:
- allow_admin: # https://nginx.org/en/docs/http/ngx_http_access_module.html#allow
- - 0.0.0.0/0 # We need to restrict ip access rules for security. 0.0.0.0/0 is for test.
+ etcd:
+ image: bitnami/etcd:3.5
+ restart: always
+ volumes:
+ - etcd_data:/bitnami/etcd
+ environment:
+ ETCD_ENABLE_V2: 'true'
+ ALLOW_NONE_AUTHENTICATION: 'yes'
+ ETCD_ADVERTISE_CLIENT_URLS: 'http://etcd:2379'
+ ETCD_LISTEN_CLIENT_URLS: 'http://0.0.0.0:2379'
+ networks:
+ - apisix
- admin_key:
- - name: "admin"
- key: edd1c9f034335f136f87ad84b625c8f1
- role: admin # admin: manage all configuration data
+networks:
+ apisix:
+ driver: bridge
- etcd:
- host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster.
- - "http://etcd:2379" # multiple etcd address
- prefix: "/apisix" # apisix configurations prefix
- timeout: 30 # 30 seconds
+volumes:
+ etcd_data:
diff --git a/.devcontainer/apisix_conf.yml b/e2e/server/docker-compose.yml
similarity index 51%
copy from .devcontainer/apisix_conf.yml
copy to e2e/server/docker-compose.yml
index 94532cf57..0e558a548 100644
--- a/.devcontainer/apisix_conf.yml
+++ b/e2e/server/docker-compose.yml
@@ -15,28 +15,32 @@
# limitations under the License.
#
-apisix:
- node_listen: 9080 # APISIX listening port
- enable_ipv6: false
- proxy_mode: http&stream
- stream_proxy:
- tcp:
- - 9100
- udp:
- - 9200
-
-deployment:
- admin:
- allow_admin: # https://nginx.org/en/docs/http/ngx_http_access_module.html#allow
- - 0.0.0.0/0 # We need to restrict ip access rules for security. 0.0.0.0/0 is for test.
-
- admin_key:
- - name: "admin"
- key: edd1c9f034335f136f87ad84b625c8f1
- role: admin # admin: manage all configuration data
-
- etcd:
- host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster.
- - "http://etcd:2379" # multiple etcd address
- prefix: "/apisix" # apisix configurations prefix
- timeout: 30 # 30 seconds
+include:
+ - docker-compose.common.yml
+services:
+ dashboard-e2e:
+ build:
+ context: ../..
+ dockerfile: e2e/server/Dockerfile
+ volumes:
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
+ networks:
+ - apisix
+ ports:
+ - '6174:6174'
+ depends_on:
+ - apisix
+ healthcheck:
+ test:
+ [
+ 'CMD',
+ 'wget',
+ '--quiet',
+ '--tries=1',
+ '--spider',
+ 'http://127.0.0.1:6174/ui/',
+ ]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
diff --git a/.devcontainer/apisix_conf.yml b/e2e/server/nginx.conf
similarity index 51%
rename from .devcontainer/apisix_conf.yml
rename to e2e/server/nginx.conf
index 94532cf57..1fde87e09 100644
--- a/.devcontainer/apisix_conf.yml
+++ b/e2e/server/nginx.conf
@@ -15,28 +15,22 @@
# limitations under the License.
#
-apisix:
- node_listen: 9080 # APISIX listening port
- enable_ipv6: false
- proxy_mode: http&stream
- stream_proxy:
- tcp:
- - 9100
- udp:
- - 9200
+server {
+ listen 6174;
-deployment:
- admin:
- allow_admin: # https://nginx.org/en/docs/http/ngx_http_access_module.html#allow
- - 0.0.0.0/0 # We need to restrict ip access rules for security. 0.0.0.0/0 is for test.
+ location /ui {
+ rewrite ^/ui$ /ui/ permanent;
+ }
- admin_key:
- - name: "admin"
- key: edd1c9f034335f136f87ad84b625c8f1
- role: admin # admin: manage all configuration data
+ location /ui/ {
+ alias /usr/share/nginx/html/;
+ index index.html;
+ try_files $uri $uri/ /ui/index.html;
+ }
- etcd:
- host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster.
- - "http://etcd:2379" # multiple etcd address
- prefix: "/apisix" # apisix configurations prefix
- timeout: 30 # 30 seconds
+ location / {
+ proxy_pass http://apisix:9180;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+}
diff --git a/src/components/form/Btn.tsx b/e2e/utils/common.ts
similarity index 60%
copy from src/components/form/Btn.tsx
copy to e2e/utils/common.ts
index f429338fa..6646fc216 100644
--- a/src/components/form/Btn.tsx
+++ b/e2e/utils/common.ts
@@ -14,11 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Button, type ButtonProps } from '@mantine/core';
-import { useFormContext, useFormState } from 'react-hook-form';
+import { readFile } from 'node:fs/promises';
+import path from 'node:path';
-export const FormSubmitBtn = (props: ButtonProps) => {
- const form = useFormContext();
- const { isSubmitting } = useFormState(form);
- return <Button type="submit" loading={isSubmitting} {...props} />;
+import { parse } from 'yaml';
+
+type APISIXConf = {
+ deployment: { admin: { admin_key: { key: string }[] } };
+};
+export const getAPISIXConf = async () => {
+ const currentDir = new URL('.', import.meta.url).pathname;
+ const confPath = path.join(currentDir, '../server/apisix_conf.yml');
+ const file = await readFile(confPath, 'utf-8');
+ const res = parse(file) as APISIXConf;
+ return { adminKey: res.deployment.admin.admin_key[0].key };
};
diff --git a/src/components/form/Btn.tsx b/e2e/utils/env.ts
similarity index 60%
copy from src/components/form/Btn.tsx
copy to e2e/utils/env.ts
index f429338fa..58b6fc6f5 100644
--- a/src/components/form/Btn.tsx
+++ b/e2e/utils/env.ts
@@ -14,11 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Button, type ButtonProps } from '@mantine/core';
-import { useFormContext, useFormState } from 'react-hook-form';
+import { config } from 'dotenv';
+import { parseEnv } from 'znv';
+import { z } from 'zod';
-export const FormSubmitBtn = (props: ButtonProps) => {
- const form = useFormContext();
- const { isSubmitting } = useFormState(form);
- return <Button type="submit" loading={isSubmitting} {...props} />;
-};
+import { BASE_PATH } from '../../src/config/constant';
+
+config({
+ path: ['./.env', './.env.local', './.env.development.local'],
+});
+
+export const env = parseEnv(process.env, {
+ E2E_TARGET_URL: z
+ .string()
+ .url()
+ .default(`http://localhost:6174${BASE_PATH}/`)
+ .describe(
+ `If you want to access the test server from dev container playwright to host e2e server, try http://host.docker.internal:6174${BASE_PATH}/`
+ ),
+});
diff --git a/e2e/utils/test.ts b/e2e/utils/test.ts
new file mode 100644
index 000000000..9f0ef0bce
--- /dev/null
+++ b/e2e/utils/test.ts
@@ -0,0 +1,70 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { test as baseTest } from '@playwright/test';
+import { getAPISIXConf } from './common';
+import path from 'node:path';
+import { existsSync } from 'node:fs';
+import { env } from './env';
+
+export const test = baseTest.extend<{}, { workerStorageState: string }>({
+ storageState: ({ workerStorageState }, use) => use(workerStorageState),
+ workerStorageState: [
+ async ({ browser }, use) => {
+ // Use parallelIndex as a unique identifier for each worker.
+ const id = test.info().parallelIndex;
+ const fileName = path.resolve(
+ test.info().project.outputDir,
+ `.auth/${id}.json`
+ );
+
+ if (existsSync(fileName)) {
+ return await use(fileName);
+ }
+
+ const page = await browser.newPage({ storageState: undefined });
+
+ // have to use env here, because the baseURL is not available in worker
+ await page.goto(env.E2E_TARGET_URL);
+
+ // we need to authenticate
+ const settingsModal = page.getByRole('dialog', { name: 'Settings' });
+ if (await settingsModal.isVisible()) {
+ const { adminKey } = await getAPISIXConf();
+
+ const adminKeyInput = page.getByRole('textbox', { name: 'Admin Key' });
+ await adminKeyInput.clear();
+ await adminKeyInput.fill(adminKey);
+ await page
+ .getByRole('dialog', { name: 'Settings' })
+ .getByRole('button')
+ .click();
+
+ await page.reload();
+ await page.waitForLoadState('networkidle');
+ }
+
+ await page.context().storageState({ path: fileName });
+ await page.close();
+ await use(fileName);
+ },
+ { scope: 'worker' },
+ ],
+ page: async ({ baseURL, page }, use) => {
+ await page.goto(baseURL);
+ await use(page);
+ },
+});
diff --git a/eslint.config.js b/eslint.config.js
index 23f5d799b..caf018beb 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,19 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
import js from '@eslint/js'
-import globals from 'globals'
+import headers from "eslint-plugin-headers";
+import importPlugin from 'eslint-plugin-import';
+import playwright from 'eslint-plugin-playwright'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
-import tseslint from 'typescript-eslint'
-import unusedImports from 'eslint-plugin-unused-imports'
import simpleImportSort from 'eslint-plugin-simple-import-sort'
-import importPlugin from 'eslint-plugin-import';
-import headers from "eslint-plugin-headers";
+import unusedImports from 'eslint-plugin-unused-imports'
+import globals from 'globals'
+import tseslint from 'typescript-eslint'
+
+const importRules = tseslint.config({
+ plugins: {
+ "unused-imports": unusedImports,
+ "simple-import-sort": simpleImportSort,
+ "import": importPlugin,
+ },
+ rules: {
+ "no-unused-vars": "off",
+ "unused-imports/no-unused-imports": "error",
+ "unused-imports/no-unused-vars": [
+ "warn",
+ {
+ "vars": "all",
+ "varsIgnorePattern": "^_",
+ "args": "after-used",
+ "argsIgnorePattern": "^_",
+ },
+ ],
+ "simple-import-sort/imports": "error",
+ "simple-import-sort/exports": "error",
+ "import/first": "error",
+ "import/newline-after-import": "error",
+ "import/no-duplicates": "error",
+ "import/no-unresolved": 'off',
+ "import/no-named-as-default-member": 'off',
+ }
+})
+
+const e2eRules = tseslint.config({
+ extends: [importRules],
+ ...playwright.configs['flat/recommended'],
+ files: ['e2e/**/*.spec.ts'],
+ rules: {
+ ...playwright.configs['flat/recommended'].rules,
+ },
+});
export default tseslint.config(
{ ignores: ['dist', 'src/routeTree.gen.ts'] },
+ e2eRules,
{
- extends: [js.configs.recommended, ...tseslint.configs.recommended],
- files: ['**/*.{ts,tsx}'],
+ extends: [js.configs.recommended, ...tseslint.configs.recommended, importRules],
+ files: ['src/**/*.{ts,tsx}', 'eslint.config.js'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
diff --git a/package.json b/package.json
index 573c43354..5cc86b491 100644
--- a/package.json
+++ b/package.json
@@ -6,9 +6,11 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
- "lint": "eslint .",
+ "lint": "eslint . --cache --max-warnings=0 --no-warn-ignored",
+ "lint:fix": "pnpm lint --fix",
"preview": "vite preview",
- "prepare": "husky"
+ "prepare": "husky",
+ "e2e": "playwright test"
},
"dependencies": {
"@ant-design/pro-components": "^2.8.7",
@@ -25,6 +27,7 @@
"axios": "^1.8.4",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
+ "dotenv": "^16.5.0",
"fast-clean": "^1.4.0",
"i18next": "^25.0.1",
"immer": "^10.1.1",
@@ -41,13 +44,16 @@
"react-hook-form": "^7.56.1",
"react-i18next": "^15.4.1",
"react-use": "^17.6.0",
+ "znv": "^0.5.0",
"zod": "^3.24.3",
"zod-empty": "^1.4.4"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
+ "@estruyf/github-actions-reporter": "^1.10.0",
"@hookform/devtools": "^4.4.0",
"@iconify-json/material-symbols": "^1.2.20",
+ "@playwright/test": "^1.52.0",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@tanstack/react-query-devtools": "^5.74.4",
@@ -61,6 +67,7 @@
"eslint": "^9.22.0",
"eslint-plugin-headers": "^1.2.1",
"eslint-plugin-import": "^2.31.0",
+ "eslint-plugin-playwright": "^2.2.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
@@ -76,11 +83,12 @@
"unplugin-icons": "^22.1.0",
"unplugin-info": "^1.2.2",
"vite": "^6.3.5",
- "vite-tsconfig-paths": "^5.1.4"
+ "vite-tsconfig-paths": "^5.1.4",
+ "yaml": "^2.7.1"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
- "eslint --fix --max-warnings=0 --no-warn-ignored"
+ "pnpm lint:fix"
]
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 000000000..f517f239b
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { defineConfig, devices } from '@playwright/test';
+
+import { env } from './e2e/utils/env';
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ tsconfig: './tsconfig.e2e.json',
+ testDir: './e2e',
+ outputDir: './test-results',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: [
+ ['html'],
+ ['list'],
+ [
+ '@estruyf/github-actions-reporter',
+ {
+ useDetails: true,
+ showError: true,
+ },
+ ],
+ ],
+ use: {
+ baseURL: env.E2E_TARGET_URL,
+ trace: 'on-first-retry',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ viewport: { width: 1920, height: 1080 },
+ permissions: ['clipboard-read'],
+ // use chrome
+ // channel: "chrome",
+ },
+ },
+ ],
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 908a984ec..2b7effbc0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -50,6 +50,9 @@ importers:
dayjs:
specifier: ^1.11.13
version: 1.11.13
+ dotenv:
+ specifier: ^16.5.0
+ version: 16.5.0
fast-clean:
specifier: ^1.4.0
version: 1.4.0
@@ -98,6 +101,9 @@ importers:
react-use:
specifier: ^17.6.0
version: 17.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ znv:
+ specifier: ^0.5.0
+ version: 0.5.0(zod@3.24.3)
zod:
specifier: ^3.24.3
version: 3.24.3
@@ -108,12 +114,18 @@ importers:
'@eslint/js':
specifier: ^9.22.0
version: 9.25.1
+ '@estruyf/github-actions-reporter':
+ specifier: ^1.10.0
+ version: 1.10.0(@playwright/test@1.52.0)
'@hookform/devtools':
specifier: ^4.4.0
version: 4.4.0(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@iconify-json/material-symbols':
specifier: ^1.2.20
version: 1.2.20
+ '@playwright/test':
+ specifier: ^1.52.0
+ version: 1.52.0
'@svgr/core':
specifier: ^8.1.0
version: 8.1.0(typescript@5.8.3)
@@ -153,6 +165,9 @@ importers:
eslint-plugin-import:
specifier: ^2.31.0
version: 2.31.0(@typescript-eslint/parser@8.32.0(eslint@9.25.1)(typescript@5.8.3))(eslint@9.25.1)
+ eslint-plugin-playwright:
+ specifier: ^2.2.0
+ version: 2.2.0(eslint@9.25.1)
eslint-plugin-react:
specifier: ^7.37.5
version: 7.37.5(eslint@9.25.1)
@@ -201,9 +216,24 @@ importers:
vite-tsconfig-paths:
specifier: ^5.1.4
version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.14.1)(sugarss@4.0.1(postcss@8.5.3))(tsx@4.19.3)(yaml@2.7.1))
+ yaml:
+ specifier: ^2.7.1
+ version: 2.7.1
packages:
+ '@actions/core@1.11.1':
+ resolution: {integrity: sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==}
+
+ '@actions/exec@1.1.1':
+ resolution: {integrity: sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==}
+
+ '@actions/http-client@2.2.3':
+ resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==}
+
+ '@actions/io@1.1.3':
+ resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
+
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
@@ -710,6 +740,15 @@ packages:
resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@estruyf/github-actions-reporter@1.10.0':
+ resolution: {integrity: sha512-u54QKI8TgUqkhNpOjyNh0hFktFSQhiQH1hs9o8jJ/FwCTlJeNCryzknzP9+B1R/jeg0cXY9QuwLcJNZCBc+Ajw==}
+ peerDependencies:
+ '@playwright/test': ^1.42.1
+
+ '@fastify/busboy@2.1.1':
+ resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
+ engines: {node: '>=14'}
+
'@floating-ui/core@1.7.0':
resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==}
@@ -850,6 +889,11 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@playwright/test@1.52.0':
+ resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@rc-component/async-validator@5.0.4':
resolution: {integrity: sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==}
engines: {node: '>=14.x'}
@@ -1395,6 +1439,11 @@ packages:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
+ ansi-to-html@0.7.2:
+ resolution: {integrity: sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==}
+ engines: {node: '>=8.0.0'}
+ hasBin: true
+
ansis@3.17.0:
resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==}
engines: {node: '>=14'}
@@ -1687,6 +1736,10 @@ packages:
dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
+ dotenv@16.5.0:
+ resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
+ engines: {node: '>=12'}
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1697,6 +1750,9 @@ packages:
emoji-regex@10.4.0:
resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
+ entities@2.2.0:
+ resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
+
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@@ -1796,6 +1852,12 @@ packages:
'@typescript-eslint/parser':
optional: true
+ eslint-plugin-playwright@2.2.0:
+ resolution: {integrity: sha512-qSQpAw7RcSzE3zPp8FMGkthaCWovHZ/BsXtpmnGax9vQLIovlh1bsZHEa2+j2lv9DWhnyeLM/qZmp7ffQZfQvg==}
+ engines: {node: '>=16.6.0'}
+ peerDependencies:
+ eslint: '>=8.40.0'
+
eslint-plugin-react-hooks@5.2.0:
resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
engines: {node: '>=10'}
@@ -1951,6 +2013,11 @@ packages:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2015,6 +2082,10 @@ packages:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'}
+ globals@13.24.0:
+ resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
+ engines: {node: '>=8'}
+
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -2357,6 +2428,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ marked@12.0.2:
+ resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==}
+ engines: {node: '>= 18'}
+ hasBin: true
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -2593,6 +2669,16 @@ packages:
pkg-types@2.1.0:
resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==}
+ playwright-core@1.52.0:
+ resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.52.0:
+ resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -3350,10 +3436,18 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
+ tunnel@0.0.6:
+ resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
+ engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
+ type-fest@0.20.2:
+ resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
+ engines: {node: '>=10'}
+
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
@@ -3396,6 +3490,10 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+ undici@5.29.0:
+ resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
+ engines: {node: '>=14.0'}
+
unplugin-icons@22.1.0:
resolution: {integrity: sha512-ect2ZNtk1Zgwb0NVHd0C1IDW/MV+Jk/xaq4t8o6rYdVS3+L660ZdD5kTSQZvsgdwCvquRw+/wYn75hsweRjoIA==}
peerDependencies:
@@ -3623,6 +3721,11 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ znv@0.5.0:
+ resolution: {integrity: sha512-o4s0CemQU7+ByxBQQnqe72QtlVDsw1GAPxBbTCbQP3sNe7/5bztRJ8fUf2dbOuhZrMapWe82hpOTBZ7QVnjZ2g==}
+ peerDependencies:
+ zod: ^3.24.2
+
zod-empty@1.4.4:
resolution: {integrity: sha512-TXHS6P/HoFsF4pft4d/6LvQ2meCkgMrAtdG7Hz9RzGt3qBjTCWK+A616MmJBSxzP3mCp8EXxyMIyA1vVzyEJgA==}
engines: {node: '>=20.10.0', npm: '>=8.1.0'}
@@ -3635,6 +3738,22 @@ packages:
snapshots:
+ '@actions/core@1.11.1':
+ dependencies:
+ '@actions/exec': 1.1.1
+ '@actions/http-client': 2.2.3
+
+ '@actions/exec@1.1.1':
+ dependencies:
+ '@actions/io': 1.1.3
+
+ '@actions/http-client@2.2.3':
+ dependencies:
+ tunnel: 0.0.6
+ undici: 5.29.0
+
+ '@actions/io@1.1.3': {}
+
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.8
@@ -4257,6 +4376,15 @@ snapshots:
'@eslint/core': 0.13.0
levn: 0.4.1
+ '@estruyf/github-actions-reporter@1.10.0(@playwright/test@1.52.0)':
+ dependencies:
+ '@actions/core': 1.11.1
+ '@playwright/test': 1.52.0
+ ansi-to-html: 0.7.2
+ marked: 12.0.2
+
+ '@fastify/busboy@2.1.1': {}
+
'@floating-ui/core@1.7.0':
dependencies:
'@floating-ui/utils': 0.2.9
@@ -4421,6 +4549,10 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
+ '@playwright/test@1.52.0':
+ dependencies:
+ playwright: 1.52.0
+
'@rc-component/async-validator@5.0.4':
dependencies:
'@babel/runtime': 7.27.0
@@ -4956,6 +5088,10 @@ snapshots:
ansi-styles@6.2.1: {}
+ ansi-to-html@0.7.2:
+ dependencies:
+ entities: 2.2.0
+
ansis@3.17.0: {}
antd@5.24.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
@@ -5340,6 +5476,8 @@ snapshots:
no-case: 3.0.4
tslib: 2.8.1
+ dotenv@16.5.0: {}
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -5350,6 +5488,8 @@ snapshots:
emoji-regex@10.4.0: {}
+ entities@2.2.0: {}
+
entities@4.5.0: {}
environment@1.1.0: {}
@@ -5543,6 +5683,11 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
+ eslint-plugin-playwright@2.2.0(eslint@9.25.1):
+ dependencies:
+ eslint: 9.25.1
+ globals: 13.24.0
+
eslint-plugin-react-hooks@5.2.0(eslint@9.25.1):
dependencies:
eslint: 9.25.1
@@ -5729,6 +5874,9 @@ snapshots:
es-set-tostringtag: 2.1.0
mime-types: 2.1.35
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -5800,6 +5948,10 @@ snapshots:
globals@11.12.0: {}
+ globals@13.24.0:
+ dependencies:
+ type-fest: 0.20.2
+
globals@14.0.0: {}
globals@15.15.0: {}
@@ -6136,6 +6288,8 @@ snapshots:
dependencies:
yallist: 3.1.1
+ marked@12.0.2: {}
+
math-intrinsics@1.1.0: {}
mdn-data@2.0.14: {}
@@ -6373,6 +6527,14 @@ snapshots:
exsolve: 1.0.5
pathe: 2.0.3
+ playwright-core@1.52.0: {}
+
+ playwright@1.52.0:
+ dependencies:
+ playwright-core: 1.52.0
+ optionalDependencies:
+ fsevents: 2.3.2
+
possible-typed-array-names@1.1.0: {}
postcss-js@4.0.1(postcss@8.5.3):
@@ -7268,10 +7430,14 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
+ tunnel@0.0.6: {}
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
+ type-fest@0.20.2: {}
+
type-fest@4.41.0: {}
typed-array-buffer@1.0.3:
@@ -7330,6 +7496,10 @@ snapshots:
undici-types@6.21.0: {}
+ undici@5.29.0:
+ dependencies:
+ '@fastify/busboy': 2.1.1
+
unplugin-icons@22.1.0(@svgr/core@8.1.0(typescript@5.8.3)):
dependencies:
'@antfu/install-pkg': 1.0.0
@@ -7514,6 +7684,10 @@ snapshots:
yocto-queue@0.1.0: {}
+ znv@0.5.0(zod@3.24.3):
+ dependencies:
+ zod: 3.24.3
+
zod-empty@1.4.4(typescript@5.8.3)(zod@3.24.3):
dependencies:
just-clone: 6.2.0
diff --git a/src/components/form/Btn.tsx b/src/components/form/Btn.tsx
index f429338fa..ac25ab41b 100644
--- a/src/components/form/Btn.tsx
+++ b/src/components/form/Btn.tsx
@@ -14,10 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Button, type ButtonProps } from '@mantine/core';
+import {
+ Button,
+ type ButtonProps,
+ type PolymorphicComponentProps,
+} from '@mantine/core';
import { useFormContext, useFormState } from 'react-hook-form';
-export const FormSubmitBtn = (props: ButtonProps) => {
+export const FormSubmitBtn = (
+ props: PolymorphicComponentProps<'button', ButtonProps>
+) => {
const form = useFormContext();
const { isSubmitting } = useFormState(form);
return <Button type="submit" loading={isSubmitting} {...props} />;
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 209dd64ed..3614a202f 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -31,5 +31,8 @@
"unplugin-icons/types/react",
]
},
- "include": ["src"]
+ "include": [
+ "src",
+ "e2e/utils/env.ts"
+ ]
}
diff --git a/tsconfig.e2e.json b/tsconfig.e2e.json
new file mode 100644
index 000000000..beb40ddac
--- /dev/null
+++ b/tsconfig.e2e.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "lib": [
+ "es2021",
+ "dom"
+ ],
+ "allowJs": true,
+ "sourceMap": false,
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "baseUrl": ".",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "noImplicitAny": false,
+ "paths": {
+ "@utils/*": [
+ "./e2e/utils/*"
+ ]
+ }
+ },
+ "include": [
+ "e2e/**/*.ts",
+ ]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 1ffef600d..7827100ee 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,6 +2,11 @@
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
+ {
+ "path": "./tsconfig.node.json"
+ },
+ {
+ "path": "./tsconfig.e2e.json"
+ }
]
}