diff --git a/.github/workflows/check-code-style.yml b/.github/workflows/check-code-style.yml index 9d7c7387d..62ed6fb43 100644 --- a/.github/workflows/check-code-style.yml +++ b/.github/workflows/check-code-style.yml @@ -25,6 +25,11 @@ jobs: */*/node_modules key: 2022-05-07-${{ runner.os }}-${{ hashFiles('**/yarn.lock')}} + - name: Install Linux Dependencies + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev libusb-1.0-0-dev + - name: Bootstrap run: | yarn diff --git a/.github/workflows/check_storybook.yml b/.github/workflows/check_storybook.yml index 3f8f61642..7c22d1ecf 100644 --- a/.github/workflows/check_storybook.yml +++ b/.github/workflows/check_storybook.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: node: - - lts/* + - 22 os: - macos-latest - ubuntu-latest diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..9d16f3e5d --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,79 @@ +name: E2E Tests + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + playwright-electron: + name: Playwright Electron E2E + runs-on: ubuntu-latest + + steps: + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "yarn" + + - name: Restore + uses: actions/cache@v4 + with: + path: | + node_modules + */*/node_modules + key: 2022-12-21-${{ runner.os }}-${{ hashFiles('**/yarn.lock')}} + + - name: Install Linux Dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libasound2t64 \ + libgbm1 \ + libgtk-3-0 \ + libnss3 \ + libudev-dev \ + libusb-1.0-0-dev \ + libx11-xcb1 \ + libxss1 \ + xvfb + + - name: Install Lerna + run: yarn global add lerna + + - name: Bootstrap + run: yarn + env: + CI: false + + - name: Run Playwright Electron E2E + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" yarn test:e2e + env: + CI: true + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report + if-no-files-found: ignore + retention-days: 14 + + - name: Upload Playwright test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-test-results + path: test-results/e2e + if-no-files-found: ignore + retention-days: 14 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 88f140a10..60f25ed52 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: node: - - lts/* + - 22 os: - macos-latest - ubuntu-latest diff --git a/.gitignore b/.gitignore index 7e8a5d078..ed7fe39ac 100755 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ node_modules # testing /**/coverage *.snap +playwright-report +test-results # production build diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 000000000..2b9dac596 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,41 @@ +# Neuron Playwright E2E + +This directory contains the release-regression E2E harness for the Electron wallet. + +## Run + +```bash +yarn install +yarn test:e2e +``` + +Useful variants: + +```bash +yarn test:e2e:headed +yarn test:e2e:ui +``` + +The root Playwright config starts the Vite UI server on `http://127.0.0.1:3000`. The fixture then launches the Electron main process from `packages/neuron-wallet` after `yarn build:main` has compiled it. + +## CI and Release Machines + +These tests are intended to run on developer machines and on CI/release workers that provide a graphical session. On Linux CI, run them under Xvfb: + +```bash +xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" yarn test:e2e +``` + +The test config is CI-aware: it uses one worker, enables retries when `CI=true`, keeps reports closed in non-interactive runs, and stores traces, screenshots, and videos in Playwright output directories. A plain headless Node environment without a GUI session or Xvfb is not enough because Playwright launches the Electron app window. + +GitHub Actions runs this through `.github/workflows/e2e.yml`. When a run fails, download the `playwright-report` and `playwright-test-results` artifacts from the workflow run. The HTML report shows the failed step, and `test-results/e2e` contains retained screenshots, videos, and Playwright trace files that can be replayed with Playwright trace viewer. + +## Test Data + +Each test run isolates Electron user data through `XDG_CONFIG_HOME` under the Playwright test output directory. Keep tests independent and avoid relying on a developer's local Neuron profile. + +For cases that need wallet, chain, or RPC state, prefer explicit setup helpers in `e2e/fixtures` over manually prepared local data. This keeps release regression repeatable in CI. + +## Selector Guidance + +Prefer stable user-facing locators (`getByRole`, `getByText`, labels) when the UI exposes them. If a flow needs precision that current markup cannot support, add explicit `data-testid` attributes near the component being tested instead of depending on generated class names or translated copy. diff --git a/e2e/fixtures/electron.ts b/e2e/fixtures/electron.ts new file mode 100644 index 000000000..f740e8fc3 --- /dev/null +++ b/e2e/fixtures/electron.ts @@ -0,0 +1,39 @@ +import path from 'path' +import { test as base, expect, type Page } from '@playwright/test' +import { _electron as electron, type ElectronApplication } from 'playwright' + +type NeuronFixtures = { + electronApp: ElectronApplication + page: Page +} + +const repoRoot = path.resolve(__dirname, '../..') +const walletPackageDir = path.join(repoRoot, 'packages/neuron-wallet') + +export const test = base.extend({ + electronApp: async ({}, use, testInfo) => { + const xdgConfigHome = testInfo.outputPath('xdg-config-home') + const electronApp = await electron.launch({ + args: [walletPackageDir], + cwd: repoRoot, + env: { + ...process.env, + BROWSER: 'none', + DISABLE_ESLINT_PLUGIN: 'true', + NODE_ENV: 'development', + XDG_CONFIG_HOME: xdgConfigHome, + }, + }) + + await use(electronApp) + await electronApp.close() + }, + + page: async ({ electronApp }, use) => { + const page = await electronApp.firstWindow({ timeout: 60_000 }) + await page.waitForLoadState('domcontentloaded') + await use(page) + }, +}) + +export { expect } diff --git a/e2e/neuron-functional-regression-plan.md b/e2e/neuron-functional-regression-plan.md new file mode 100644 index 000000000..970c2bab6 --- /dev/null +++ b/e2e/neuron-functional-regression-plan.md @@ -0,0 +1,65 @@ +# Neuron 功能测试与发版回归清单 + +本清单结合现有 Jest/Vitest 覆盖、附件中的历史测试用例,以及 Neuron 当前功能模块整理。目标是把发版回归拆成稳定可自动化的 Playwright E2E、需要链上数据的集成用例、以及仍需人工设备验证的专项用例。 + +## 覆盖分层 + +| 层级 | 目标 | 现状 | 建议执行 | +| --- | --- | --- | --- | +| 单元测试 | 纯函数、校验器、格式化、模型计算 | `packages/neuron-ui/src/tests`、`packages/neuron-wallet/tests` 已覆盖大量工具、控制器、服务 | 每次 PR 执行 `yarn test` | +| 集成测试 | wallet service、同步、交易生成、数据库迁移、RPC mock | wallet 包已有 `.intg.test.ts` 和 controller/service 测试 | 每次发版候选执行 `yarn test:ci` | +| E2E 冒烟 | Electron 启动、主窗口、路由、核心页面不崩溃 | 本次新增 Playwright 框架 | 每次发版候选执行 `yarn test:e2e` | +| E2E 业务回归 | 钱包创建/导入、同步、转账、历史、DAO、多签、UDT | 需要补充稳定测试数据和选择器 | 按下面 P0/P1 分批自动化 | +| 人工专项 | Ledger/硬件钱包、系统托盘、安装包升级、平台差异 | 依赖设备或 OS 行为 | 发版前人工验收 | + +## P0 自动化回归 + +| 模块 | 场景 | 断言 | +| --- | --- | --- | +| 启动 | 启动 Electron 并连接本地 Vite UI | 主窗口标题为 Neuron,`#root` 渲染,renderer 无明显崩溃文本 | +| 钱包入口 | 无本地钱包数据时进入钱包向导 | 展示新建、助记词导入、keystore 导入、硬件钱包入口 | +| 新建钱包 | 新建钱包,记录助记词,设置名称和密码 | 钱包创建成功,进入概览,默认网络和同步状态可见 | +| 导入钱包 | 使用固定测试助记词导入钱包 | 导入成功,地址派生结果与固定期望一致 | +| 地址派生 | receiving 第 17 个地址被使用后触发扩展 | receiving 地址补齐到 20 个可用地址,change 地址保持 10 个基础窗口 | +| 同步正确性 | 轻节点测试网/远程测试节点同步同一测试钱包 | 余额、交易数、交易状态与测试链浏览器或 mock RPC 账本一致 | +| 转账表单 | 合法/非法收款地址、金额、小于最小 cell 容量、余额不足 | 合法输入可进入确认页;非法输入展示对应错误 | +| Max 转账 | 单收款地址 Max、多个收款地址中一个 Max | Max 金额等于余额减手续费,Max 字段不可手动改写 | +| 发送交易 | 正确密码发送、错误密码发送 | 正确密码广播成功进入历史;错误密码展示密码错误且不广播 | +| 历史 | 搜索地址/hash、排序、分页、展开详情 | 展示钱包名称、类型、金额、时间、状态,input/output 金额正确 | + +## P1 自动化回归 + +| 模块 | 场景 | 断言 | +| --- | --- | --- | +| 网络 | 轻节点测试网、远程测试节点、主网/测试网切换 | 网络切换后同步任务和钱包监听脚本正确刷新 | +| 数据路径 | 修改 CKB 数据目录、首次同步、已有数据目录 | 提示文案正确,配置文件和数据目录落到预期位置 | +| 资产账户 | 创建 CKB asset account、创建 SUDT account | 创建成功,账户列表和余额展示正确 | +| SUDT | SUDT 收款、转账、迁移、回收 | token id、金额精度、手续费和历史记录正确 | +| Cell 管理 | 选择 cell、合并、消耗、锁定 cell 展示 | input/output 和手续费计算正确 | +| DAO | 存入、可解锁、提取、收益展示 | DAO 阶段、APC、可提取金额、历史详情正确 | +| 多签 | 新建多签地址、导入配置、发送多签交易 | 多签地址同步、签名阈值、交易状态正确 | +| 离线签名 | 导出待签名交易、导入签名、广播 | 文件内容合法,广播成功后历史状态正确 | +| 签名验证 | 普通钱包签名/验签 | 签名结果和验证结果正确 | +| Debug 导出 | 导出 debug 信息 | 归档包含必要日志且不包含敏感私钥 | + +## P2/人工专项 + +| 模块 | 场景 | 说明 | +| --- | --- | --- | +| 硬件钱包 | Ledger 连接、导入、签名、断开重连 | 需要硬件设备和不同 OS 权限 | +| 安装包 | macOS/Windows/Linux 安装、升级、卸载 | 需要平台安装包和系统级校验 | +| 自动更新 | 检查更新、下载、安装 | 需要 release server 或 mock update feed | +| 进程管理 | bundled CKB/light client 启停、异常恢复 | 需要真实二进制和端口冲突场景 | +| 大数据量 | 几千个地址、大量交易历史 | 建议使用固定 mock RPC 或预置链数据 | + +## Playwright 自动化路线 + +1. 保持 `e2e/specs/app-smoke.spec.ts` 作为最小发版门禁,验证 Electron + UI 的启动链路。 +2. 在 UI 关键按钮、输入框、列表行补充稳定 `data-testid`,优先覆盖钱包向导、发送页、历史页。 +3. 新增 `e2e/fixtures/wallet-data.ts`,提供固定助记词、钱包名称、密码和期望地址。 +4. 新增 mock RPC/light-client fixture,先自动化同步、余额、历史展示,再接真实测试网 nightly。 +5. 把链上依赖用例拆成 `@mock-chain` 和 `@testnet` 标签,CI 默认跑 mock,发版候选跑 testnet。 + +## 发版建议 + +P0 必须通过后再进入候选包验证;P1 覆盖主要资产和交易风险,建议每个 minor 版本完整执行;P2 由发布负责人根据平台和设备资源安排人工验收。 diff --git a/e2e/specs/app-smoke.spec.ts b/e2e/specs/app-smoke.spec.ts new file mode 100644 index 000000000..f7aefc615 --- /dev/null +++ b/e2e/specs/app-smoke.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from '../fixtures/electron' + +test.describe('Neuron release smoke', () => { + test('opens the Electron shell against the local UI server', async ({ page }) => { + await expect(page).toHaveTitle(/Neuron/) + await expect(page.locator('#root')).toBeVisible() + await expect(page.locator('body')).not.toContainText('ResizeObserver loop completed with undelivered notifications') + }) + + test('renders the wallet entry route without a renderer crash', async ({ page }) => { + await page.goto('http://127.0.0.1:3000/#/wizard/') + + await expect(page.locator('#root')).toBeVisible() + await expect(page.locator('body')).not.toContainText('Unhandled Runtime Error') + await expect(page.locator('body')).not.toContainText('Cannot read properties of undefined') + }) +}) diff --git a/package.json b/package.json index 54d4d80e8..8c0d15c6f 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,9 @@ "package:test": "yarn build && ./scripts/copy-ui-files.sh && ./scripts/package-for-test.sh", "test": "cross-env NODE_OPTIONS=--openssl-legacy-provider lerna run --parallel --load-env-files=false test", "test:ci": "yarn build:main && yarn test", + "test:e2e": "yarn build:main && playwright test", + "test:e2e:headed": "yarn build:main && playwright test --headed", + "test:e2e:ui": "yarn build:main && playwright test --ui", "lint": "lerna run --stream lint", "postinstall": "husky install", "db:chain": "node ./node_modules/.bin/typeorm", @@ -42,6 +45,7 @@ }, "devDependencies": { "@babel/core": "7.27.1", + "@playwright/test": "1.60.0", "@types/jest": "27.5.2", "@types/node": "20.10.5", "@types/npmlog": "7.0.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..0d6345d20 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from '@playwright/test' + +const isCI = !!process.env.CI + +export default defineConfig({ + testDir: './e2e/specs', + outputDir: './test-results/e2e', + fullyParallel: false, + workers: 1, + timeout: 120_000, + expect: { + timeout: 15_000, + }, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + reporter: [ + ['list'], + ['html', { open: 'never', outputFolder: 'playwright-report' }], + ], + use: { + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + webServer: { + command: 'yarn start:ui', + url: 'http://127.0.0.1:3000', + reuseExistingServer: !isCI, + timeout: 120_000, + }, +}) diff --git a/yarn.lock b/yarn.lock index 9498c4373..b1aa4a7de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3455,6 +3455,13 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@playwright/test@1.60.0": + version "1.60.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.60.0.tgz#e696c31427e8882851235cd556dc2490c3206d97" + integrity sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag== + dependencies: + playwright "1.60.0" + "@remix-run/router@1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.14.1.tgz#6d2dd03d52e604279c38911afc1079d58c50a755" @@ -9102,7 +9109,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.1.2, fsevents@~2.3.2: +fsevents@2.3.2, fsevents@^2.1.2, 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== @@ -13535,6 +13542,20 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" +playwright-core@1.60.0: + version "1.60.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.60.0.tgz#24e0d9cc4730713db5dffcace29b5e4696b1907a" + integrity sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA== + +playwright@1.60.0: + version "1.60.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.60.0.tgz#89710863a51f21112633ef8b6b182594d3bfd7b5" + integrity sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA== + dependencies: + playwright-core "1.60.0" + optionalDependencies: + fsevents "2.3.2" + plist@^3.0.4: version "3.0.6" resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.6.tgz#7cfb68a856a7834bca6dbfe3218eb9c7740145d3" @@ -15492,7 +15513,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + 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-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== @@ -15601,7 +15631,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm: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-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== @@ -16928,7 +16965,16 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==