Browse Source

feat: add playwright e2e testing framework (#4468)

* feat: add playwright e2e testing framework
Vben 7 months ago
parent
commit
2a83f1d666

+ 1 - 1
.gitpod.yml

@@ -3,4 +3,4 @@ ports:
     onOpen: open-preview
 tasks:
   - init: corepack enable && pnpm install
-    command: pnpm run dev
+    command: pnpm run dev:play

+ 4 - 0
README.ja-JP.md

@@ -133,6 +133,10 @@ pnpm build
 
 <a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
 
+## スター歴史
+
+[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date)
+
 ## 貢献者
 
 <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">

+ 4 - 0
README.md

@@ -132,6 +132,10 @@ If you think this project is helpful to you, you can help the author buy a cup o
 
 <a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
 
+## Star History
+
+[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date)
+
 ## Contributor
 
 <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">

+ 4 - 0
README.zh-CN.md

@@ -132,6 +132,10 @@ pnpm build
 
 [CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
 
+## Star History
+
+[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date)
+
 ## Contributor
 
 <a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">

+ 2 - 4
docs/src/en/guide/essentials/development.md

@@ -46,8 +46,6 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
 ```json
 {
   "scripts": {
-    // Install dependencies
-    "bootstrap": "pnpm install",
     // Build the project
     "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
     // Build the project with analysis
@@ -107,9 +105,9 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
     // Package specification check
     "publint": "vsh publint",
     // Delete all node_modules, yarn.lock, package.lock.json, and reinstall dependencies
-    "reinstall": "pnpm clean --del-lock && pnpm bootstrap",
+    "reinstall": "pnpm clean --del-lock && pnpm install",
     // Run vitest unit tests
-    "test:unit": "vitest",
+    "test:unit": "vitest run --dom",
     // Update project dependencies
     "update:deps": " pnpm update --latest --recursive",
     // Changeset generation and versioning

+ 2 - 4
docs/src/guide/essentials/development.md

@@ -46,8 +46,6 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
 ```json
 {
   "scripts": {
-    // 安装依赖
-    "bootstrap": "pnpm install",
     // 构建项目
     "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
     // 构建项目并分析
@@ -107,9 +105,9 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
     // 包规范检查
     "publint": "vsh publint",
     // 删除所有的node_modules、yarn.lock、package.lock.json,重新安装依赖
-    "reinstall": "pnpm clean --del-lock && pnpm bootstrap",
+    "reinstall": "pnpm clean --del-lock && pnpm install",
     // 运行 vitest 单元测试
-    "test:unit": "vitest",
+    "test:unit": "vitest run --dom",
     // 更新项目依赖
     "update:deps": " pnpm update --latest --recursive",
     // changeset生成提交集

+ 1 - 0
internal/lint-configs/eslint-config/src/configs/node.ts

@@ -24,6 +24,7 @@ export async function node(): Promise<Linter.Config[]> {
               'vite',
               '@vue/test-utils',
               '@vben/tailwind-config',
+              '@playwright/test',
             ],
           },
         ],

+ 8 - 0
internal/lint-configs/eslint-config/src/custom-config.ts

@@ -134,6 +134,14 @@ const customConfig: Linter.Config[] = [
       'unicorn/prefer-module': 'off',
     },
   },
+  {
+    files: ['**/**/playwright.config.ts'],
+    rules: {
+      'n/prefer-global/buffer': 'off',
+      'n/prefer-global/process': 'off',
+      'no-console': 'off',
+    },
+  },
   {
     files: ['internal/**/**'],
     rules: {

+ 8 - 8
package.json

@@ -25,11 +25,10 @@
   },
   "type": "module",
   "scripts": {
-    "bootstrap": "pnpm install",
     "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
     "build:analyze": "turbo build:analyze",
-    "build:docker": "./build-local-docker-image.sh",
     "build:antd": "pnpm run build --filter=@vben/web-antd",
+    "build:docker": "./build-local-docker-image.sh",
     "build:docs": "pnpm run build --filter=@vben/docs",
     "build:ele": "pnpm run build --filter=@vben/web-ele",
     "build:naive": "pnpm run build --filter=@vben/web-naive",
@@ -55,15 +54,16 @@
     "prepare": "is-ci || husky",
     "preview": "turbo-run preview",
     "publint": "vsh publint",
-    "reinstall": "pnpm clean --del-lock && pnpm bootstrap",
-    "test:unit": "vitest",
+    "reinstall": "pnpm clean --del-lock && pnpm install",
+    "test:unit": "vitest run --dom",
+    "test:e2e": "turbo run test:e2e",
     "update:deps": "pnpm update --latest --recursive",
     "version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile"
   },
   "devDependencies": {
     "@changesets/changelog-github": "catalog:",
     "@changesets/cli": "catalog:",
-    "@types/jsdom": "catalog:",
+    "@playwright/test": "catalog:",
     "@types/node": "catalog:",
     "@vben/commitlint-config": "workspace:*",
     "@vben/eslint-config": "workspace:*",
@@ -80,10 +80,11 @@
     "autoprefixer": "catalog:",
     "cross-env": "catalog:",
     "cspell": "catalog:",
+    "happy-dom": "catalog:",
     "husky": "catalog:",
     "is-ci": "catalog:",
-    "jsdom": "catalog:",
     "lint-staged": "catalog:",
+    "playwright": "catalog:",
     "rimraf": "catalog:",
     "tailwindcss": "catalog:",
     "turbo": "catalog:",
@@ -113,8 +114,7 @@
     },
     "neverBuiltDependencies": [
       "canvas",
-      "node-gyp",
-      "playwright"
+      "node-gyp"
     ]
   }
 }

+ 4 - 4
packages/@core/ui-kit/shadcn-ui/package.json

@@ -1,6 +1,8 @@
 {
   "name": "@vben-core/shadcn-ui",
   "version": "5.3.0",
+  "#main": "./dist/index.mjs",
+  "#module": "./dist/index.mjs",
   "homepage": "https://github.com/vbenjs/vue-vben-admin",
   "bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
   "repository": {
@@ -20,16 +22,14 @@
   "sideEffects": [
     "**/*.css"
   ],
-  "#main": "./dist/index.mjs",
   "main": "./src/index.ts",
-  "#module": "./dist/index.mjs",
   "module": "./src/index.ts",
   "exports": {
     ".": {
       "types": "./src/index.ts",
       "development": "./src/index.ts",
-      "//default": "./dist/index.mjs",
-      "default": "./src/index.ts"
+      "default": "./src/index.ts",
+      "//default": "./dist/index.mjs"
     }
   },
   "publishConfig": {

+ 1 - 0
packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-action.vue

@@ -51,6 +51,7 @@ defineExpose({
     }"
     :style="style"
     class="bg-background dark:bg-accent absolute left-0 top-0 flex h-full cursor-move items-center justify-center px-3.5 shadow-md"
+    name="captcha-action"
   >
     <Slot :is-passing="isPassing" class="text-foreground/60 size-4">
       <slot name="icon">

+ 1 - 0
packages/effects/common-ui/src/ui/authentication/forget-password.vue

@@ -95,6 +95,7 @@ function goToLogin() {
         :class="{
           'cursor-wait': loading,
         }"
+        aria-label="submit"
         class="mt-2 w-full"
         @click="handleSubmit"
       >

+ 1 - 0
packages/effects/common-ui/src/ui/authentication/login.vue

@@ -129,6 +129,7 @@ onMounted(() => {
         'cursor-wait': loading,
       }"
       :loading="loading"
+      aria-label="login"
       class="w-full"
       @click="handleSubmit"
     >

+ 1 - 0
packages/effects/common-ui/src/ui/authentication/register.vue

@@ -97,6 +97,7 @@ function goToLogin() {
         'cursor-wait': loading,
       }"
       :loading="loading"
+      aria-label="register"
       class="mt-2 w-full"
       @click="handleSubmit"
     >

+ 20 - 0
playground/__tests__/e2e/auth-login.spec.ts

@@ -0,0 +1,20 @@
+import { expect, test } from '@playwright/test';
+
+import { authLogin } from './common/auth';
+
+test.beforeEach(async ({ page }) => {
+  await page.goto('/');
+});
+
+test.describe('Auth Login Page Tests', () => {
+  test('check title and page elements', async ({ page }) => {
+    // 获取页面标题并断言标题包含 'Vben Admin'
+    const title = await page.title();
+    expect(title).toContain('Vben Admin');
+  });
+
+  // 测试用例: 成功登录
+  test('should successfully login with valid credentials', async ({ page }) => {
+    await authLogin(page);
+  });
+});

+ 46 - 0
playground/__tests__/e2e/common/auth.ts

@@ -0,0 +1,46 @@
+import type { Page } from '@playwright/test';
+
+import { expect } from '@playwright/test';
+
+export async function authLogin(page: Page) {
+  // 确保登录表单正常
+  const usernameInput = await page.locator(`input[name='username']`);
+  await expect(usernameInput).toBeVisible();
+
+  const passwordInput = await page.locator(`input[name='password']`);
+  await expect(passwordInput).toBeVisible();
+
+  const sliderCaptcha = await page.locator(`div[name='captcha']`);
+  const sliderCaptchaAction = await page.locator(`div[name='captcha-action']`);
+  await expect(sliderCaptcha).toBeVisible();
+  await expect(sliderCaptchaAction).toBeVisible();
+
+  // 拖动验证码滑块
+  // 获取拖动按钮的位置
+  const sliderCaptchaBox = await sliderCaptcha.boundingBox();
+  if (!sliderCaptchaBox) throw new Error('滑块未找到');
+
+  const actionBoundingBox = await sliderCaptchaAction.boundingBox();
+  if (!actionBoundingBox) throw new Error('要拖动的按钮未找到');
+
+  // 计算起始位置和目标位置
+  const startX = actionBoundingBox.x + actionBoundingBox.width / 2; // div 中心的 x 坐标
+  const startY = actionBoundingBox.y + actionBoundingBox.height / 2; // div 中心的 y 坐标
+
+  const targetX = startX + sliderCaptchaBox.width + actionBoundingBox.width; // 向右拖动容器的宽度
+  const targetY = startY; // y 坐标保持不变
+
+  // 模拟鼠标拖动
+  await page.mouse.move(startX, startY); // 移动到 action 的中心
+  await page.mouse.down(); // 按下鼠标
+  await page.mouse.move(targetX, targetY, { steps: 20 }); // 拖动到目标位置
+  await page.mouse.up(); // 松开鼠标
+
+  // 在拖动后进行断言,检查action是否在预期位置,
+  const newActionBoundingBox = await sliderCaptchaAction.boundingBox();
+  expect(newActionBoundingBox?.x).toBeGreaterThan(actionBoundingBox.x);
+
+  // 到这里已经校验成功,点击进行登录
+  await page.waitForTimeout(300);
+  await page.getByRole('button', { name: 'login' }).click();
+}

+ 4 - 1
playground/package.json

@@ -20,7 +20,10 @@
     "build:analyze": "pnpm vite build --mode analyze",
     "dev": "pnpm vite --mode development",
     "preview": "vite preview",
-    "typecheck": "vue-tsc --noEmit --skipLibCheck"
+    "typecheck": "vue-tsc --noEmit --skipLibCheck",
+    "test:e2e": "playwright test",
+    "test:e2e-ui": "playwright test --ui",
+    "test:e2e-codegen": "playwright codegen"
   },
   "imports": {
     "#/*": "./src/*"

+ 108 - 0
playground/playwright.config.ts

@@ -0,0 +1,108 @@
+import type { PlaywrightTestConfig } from '@playwright/test';
+
+import { devices } from '@playwright/test';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+const config: PlaywrightTestConfig = {
+  expect: {
+    /**
+     * Maximum time expect() should wait for the condition to be met.
+     * For example in `await expect(locator).toHaveText();`
+     */
+    timeout: 5000,
+  },
+  /* Fail the build on CI if you accidentally left test.only in the source code. */
+  forbidOnly: !!process.env.CI,
+  /* Folder for test artifacts such as screenshots, videos, traces, etc. */
+  outputDir: 'node_modules/.e2e/test-results/',
+  /* Configure projects for major browsers */
+  projects: [
+    {
+      name: 'chromium',
+      use: {
+        ...devices['Desktop Chrome'],
+      },
+    },
+    // {
+    //   name: 'firefox',
+    //   use: {
+    //     ...devices['Desktop Firefox'],
+    //   },
+    // },
+    // {
+    //   name: 'webkit',
+    //   use: {
+    //     ...devices['Desktop Safari'],
+    //   },
+    // },
+
+    /* Test against mobile viewports. */
+    // {
+    //   name: 'Mobile Chrome',
+    //   use: {
+    //     ...devices['Pixel 5'],
+    //   },
+    // },
+    // {
+    //   name: 'Mobile Safari',
+    //   use: {
+    //     ...devices['iPhone 12'],
+    //   },
+    // },
+
+    /* Test against branded browsers. */
+    // {
+    //   name: 'Microsoft Edge',
+    //   use: {
+    //     channel: 'msedge',
+    //   },
+    // },
+    // {
+    //   name: 'Google Chrome',
+    //   use: {
+    //     channel: 'chrome',
+    //   },
+    // },
+  ],
+  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+  reporter: [
+    ['list'],
+    ['html', { outputFolder: 'node_modules/.e2e/test-results' }],
+  ],
+  /* Retry on CI only */
+  retries: process.env.CI ? 2 : 0,
+  testDir: './__tests__/e2e',
+  /* Maximum time one test can run for. */
+  timeout: 30 * 1000,
+  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+  use: {
+    /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
+    actionTimeout: 0,
+    /* Base URL to use in actions like `await page.goto('/')`. */
+    baseURL: 'http://localhost:5555',
+    /* Only on CI systems run the tests headless */
+    headless: !!process.env.CI,
+    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+    trace: 'retain-on-failure',
+  },
+
+  /* Run your local dev server before starting the tests */
+  webServer: {
+    command: process.env.CI ? 'pnpm preview --port 5555' : 'pnpm dev',
+    port: 5555,
+    reuseExistingServer: !process.env.CI,
+  },
+
+  /* Opt out of parallel tests on CI. */
+  workers: process.env.CI ? 1 : undefined,
+};
+
+export default config;

File diff suppressed because it is too large
+ 116 - 263
pnpm-lock.yaml


+ 3 - 2
pnpm-workspace.yaml

@@ -29,6 +29,7 @@ catalog:
   '@jspm/generator': ^2.3.1
   '@manypkg/get-packages': ^2.2.2
   '@nolebase/vitepress-plugin-git-changelog': ^2.5.0
+  '@playwright/test': ^1.47.2
   '@radix-icons/vue': ^1.0.0
   '@stylistic/stylelint-plugin': ^3.0.1
   '@tailwindcss/nesting': 0.0.0-insiders.565cd3e
@@ -39,7 +40,6 @@ catalog:
   '@types/chalk': ^2.2.0
   '@types/eslint': ^9.6.1
   '@types/html-minifier-terser': ^7.0.2
-  '@types/jsdom': ^21.1.7
   '@types/jsonwebtoken': ^9.0.7
   '@types/lodash.clonedeep': ^4.5.9
   '@types/node': ^22.5.5
@@ -103,10 +103,10 @@ catalog:
   get-port: ^7.1.0
   globals: ^15.9.0
   h3: ^1.12.0
+  happy-dom: ^15.7.4
   html-minifier-terser: ^7.2.0
   husky: ^9.1.6
   is-ci: ^3.0.1
-  jsdom: ^25.0.1
   jsonc-eslint-parser: ^2.4.0
   jsonwebtoken: ^9.0.2
   lint-staged: ^15.2.10
@@ -121,6 +121,7 @@ catalog:
   pinia: 2.2.2
   pinia-plugin-persistedstate: ^4.0.2
   pkg-types: ^1.2.0
+  playwright: ^1.47.2
   postcss: ^8.4.47
   postcss-antd-fixes: ^0.2.0
   postcss-html: ^1.7.0

+ 1 - 0
turbo.json

@@ -33,6 +33,7 @@
     "stub": {
       "cache": false
     },
+    "test:e2e": {},
     "dev": {
       "dependsOn": [],
       "outputs": [],

+ 3 - 2
vitest.config.ts

@@ -1,10 +1,11 @@
 import Vue from '@vitejs/plugin-vue';
 import VueJsx from '@vitejs/plugin-vue-jsx';
-import { defineConfig } from 'vitest/config';
+import { configDefaults, defineConfig } from 'vitest/config';
 
 export default defineConfig({
   plugins: [Vue(), VueJsx()],
   test: {
-    environment: 'jsdom',
+    environment: 'happy-dom',
+    exclude: [...configDefaults.exclude, '**/e2e/**'],
   },
 });

Some files were not shown because too many files changed in this diff