From 5c82857fea956f90bb041d189da1c652a09d468f Mon Sep 17 00:00:00 2001 From: "ermin.zem" Date: Thu, 9 Jul 2026 16:11:58 +0800 Subject: [PATCH] Add harness infrastructure for web-shell package (#6517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(web-shell): add browser and lint harness * test(web-shell): harden browser smoke harness * fix(web-shell): guard mock daemon model state * test(web-shell): remove unused scenario harness * fix(web-shell): remove stale lint disables * test(web-shell): make matchMedia stub writable * fix(web-shell): exclude tests from package typecheck * test(web-shell): tighten mock daemon route contract * Update packages/web-shell/client/e2e/utils/mockDaemon.ts Co-authored-by: qwen-code-ci-bot * test(web-shell): clear stale SSE connections * ci(web-shell): gate smoke on full CI profile --------- Co-authored-by: ermin.zem Co-authored-by: qwen-code-ci-bot Co-authored-by: Shaojin Wen Co-authored-by: 易良 <1204183885@qq.com> --- .github/workflows/ci.yml | 58 ++ .github/workflows/e2e.yml | 44 + .gitignore | 2 + eslint.config.js | 76 ++ package-lock.json | 1 + packages/web-shell/client/App.tsx | 13 +- .../client/components/ChatEditor.tsx | 15 +- .../components/MessageList.dom.test.tsx | 1 + .../client/components/MessageList.test.ts | 20 +- .../client/components/MessageList.tsx | 7 +- .../web-shell/client/components/ToastHost.tsx | 13 +- .../components/dialogs/ApprovalModeDialog.tsx | 3 + .../client/components/dialogs/DialogShell.tsx | 2 + .../client/components/dialogs/ModelDialog.tsx | 3 + .../components/dialogs/ResumeDialog.tsx | 6 +- .../client/components/dialogs/SessionRow.tsx | 5 + .../client/components/dialogs/ThemeDialog.tsx | 3 + .../messages/EnhancedMarkdownTable.test.tsx | 3 +- .../components/messages/ToolApproval.tsx | 3 + .../web-shell/client/e2e/utils/mockDaemon.ts | 883 ++++++++++++++++++ .../client/e2e/utils/sseTransport.ts | 227 +++++ .../client/e2e/web-shell.smoke.spec.ts | 309 ++++++ .../client/extensions/inputHighlight.ts | 3 +- .../client/hooks/useQueuedPrompts.ts | 11 - packages/web-shell/client/index.html | 16 +- .../web-shell/client/test/reactHarness.tsx | 40 + packages/web-shell/client/test/setup.ts | 62 ++ .../web-shell/client/voice/VoiceButton.tsx | 3 +- packages/web-shell/package.json | 14 +- packages/web-shell/playwright.config.ts | 38 + packages/web-shell/tsconfig.json | 9 +- packages/web-shell/tsconfig.lib.json | 2 + packages/web-shell/vitest.config.ts | 26 + 33 files changed, 1873 insertions(+), 48 deletions(-) create mode 100644 packages/web-shell/client/e2e/utils/mockDaemon.ts create mode 100644 packages/web-shell/client/e2e/utils/sseTransport.ts create mode 100644 packages/web-shell/client/e2e/web-shell.smoke.spec.ts create mode 100644 packages/web-shell/client/test/reactHarness.tsx create mode 100644 packages/web-shell/client/test/setup.ts create mode 100644 packages/web-shell/playwright.config.ts create mode 100644 packages/web-shell/vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d40f391a9..a9df0e52c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -365,6 +365,64 @@ jobs: name: 'coverage-reports-22.x-ubuntu-latest' path: 'packages/*/coverage' + web_shell_e2e_smoke: + name: 'web-shell E2E Smoke (ubuntu-latest, Node 22.x)' + needs: + - 'classify_pr' + - 'test' + if: |- + ${{ + !cancelled() && + github.event_name == 'pull_request' && + needs.classify_pr.outputs.skip_ci != 'true' && + needs.test.outputs.ci_profile == 'full' + }} + runs-on: 'ubuntu-latest' + timeout-minutes: 20 + permissions: + contents: 'read' + steps: + - name: 'Checkout' + uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2 + with: + ref: "${{ format('refs/pull/{0}/head', github.event.pull_request.number) }}" + fetch-depth: 1 + + - name: 'Set up Node.js 22.x' + uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 + with: + node-version: '22.x' + cache: 'npm' + cache-dependency-path: 'package-lock.json' + registry-url: 'https://registry.npmjs.org/' + + - name: 'Configure npm for rate limiting' + run: |- + npm config set fetch-retry-mintimeout 20000 + npm config set fetch-retry-maxtimeout 120000 + npm config set fetch-retries 5 + npm config set fetch-timeout 300000 + + - name: 'Install dependencies' + run: |- + npm ci --prefer-offline --no-audit --progress=false + + - name: 'Install Playwright Chromium' + run: 'npx playwright install --with-deps chromium' + + - name: 'Run web-shell browser smoke' + run: 'npm run test:e2e:smoke --workspace=packages/web-shell' + + - name: 'Upload web-shell Playwright artifacts' + if: '${{ always() }}' + uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1 + with: + name: 'web-shell-e2e-smoke' + path: |- + packages/web-shell/client/e2e/test-results + packages/web-shell/client/e2e/playwright-report + if-no-files-found: 'ignore' + # macOS/Windows: slowest/costliest runners, rare platform regressions — run # only in the merge queue. Skipped on PR (ubuntu is the fast PR signal) and on # push (the queue already tested the merged tree, so a post-merge re-run is diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c5afbf1d38..8e43737886 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -144,3 +144,47 @@ jobs: OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}' run: 'npm run test:e2e' + + web-shell-browser-regression: + name: 'web-shell Browser Regression' + runs-on: 'ubuntu-latest' + if: |- + ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} + steps: + - name: 'Checkout' + uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2 + + - name: 'Set up Node.js' + uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 + with: + node-version-file: '.nvmrc' + cache: 'npm' + cache-dependency-path: 'package-lock.json' + registry-url: 'https://registry.npmjs.org/' + + - name: 'Configure npm for rate limiting' + run: |- + npm config set fetch-retry-mintimeout 20000 + npm config set fetch-retry-maxtimeout 120000 + npm config set fetch-retries 5 + npm config set fetch-timeout 300000 + + - name: 'Install dependencies' + run: |- + npm ci --prefer-offline --no-audit --progress=false + + - name: 'Install Playwright Chromium' + run: 'npx playwright install --with-deps chromium' + + - name: 'Run web-shell browser regression' + run: 'npm run test:e2e --workspace=packages/web-shell' + + - name: 'Upload web-shell Playwright artifacts' + if: '${{ always() }}' + uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1 + with: + name: 'web-shell-browser-regression' + path: |- + packages/web-shell/client/e2e/test-results + packages/web-shell/client/e2e/playwright-report + if-no-files-found: 'ignore' diff --git a/.gitignore b/.gitignore index e7e956c2c7..cb894b6007 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,8 @@ bundle # Test report files junit.xml packages/*/coverage/ +packages/web-shell/client/e2e/playwright-report/ +packages/web-shell/client/e2e/test-results/ # PR body draft pr_body.md diff --git a/eslint.config.js b/eslint.config.js index f74416e232..08dca2c70f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -170,6 +170,82 @@ export default tseslint.config( 'default-case': 'error', }, }, + { + files: [ + 'packages/web-shell/client/**/*.{ts,tsx}', + 'packages/web-shell/*.config.ts', + ], + plugins: { + import: importPlugin, + }, + settings: { + 'import/resolver': { + node: true, + }, + }, + languageOptions: { + globals: { + ...globals.browser, + ...globals.es2021, + ...globals.node, + }, + }, + rules: { + 'react/prop-types': 'off', + '@typescript-eslint/consistent-type-imports': [ + 'error', + { disallowTypeAnnotations: false }, + ], + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'no-console': ['error', { allow: ['warn', 'error'] }], + 'no-debugger': 'error', + 'object-shorthand': 'error', + 'prefer-const': ['error', { destructuring: 'all' }], + }, + }, + { + files: [ + 'packages/web-shell/client/**/*.test.{ts,tsx}', + 'packages/web-shell/client/test/**/*.{ts,tsx}', + ], + plugins: { + vitest, + }, + languageOptions: { + globals: { + ...globals.browser, + ...globals.es2021, + ...globals.vitest, + }, + }, + rules: { + ...vitest.configs.recommended.rules, + 'vitest/expect-expect': 'off', + 'vitest/no-commented-out-tests': 'off', + 'no-console': 'off', + }, + }, + { + files: ['packages/web-shell/client/e2e/**/*.{ts,tsx}'], + languageOptions: { + globals: { + ...globals.browser, + ...globals.es2021, + ...globals.node, + }, + }, + rules: { + 'no-console': 'off', + }, + }, { // Enforce kebab-case filenames files: ['packages/core/src/**/*.ts', 'packages/cli/src/**/*.ts'], diff --git a/package-lock.json b/package-lock.json index 41267ce4ed..a02e6d82f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27380,6 +27380,7 @@ "shiki": "^1.29.2" }, "devDependencies": { + "@playwright/test": "^1.57.0", "@qwen-code/sdk": "file:../sdk-typescript", "@qwen-code/webui": "file:../webui", "@types/node": "^22.0.0", diff --git a/packages/web-shell/client/App.tsx b/packages/web-shell/client/App.tsx index 6cf2b93481..d8592e0e4b 100644 --- a/packages/web-shell/client/App.tsx +++ b/packages/web-shell/client/App.tsx @@ -627,12 +627,6 @@ function isAlreadyDispatched(error: unknown): error is AlreadyDispatchedError { ); } -function logSessionNoticesHook(notices: readonly DaemonSessionNotice[]): void { - if (notices.length > 0) { - console.info('[web-shell] useSessionNotices()', { notices }); - } -} - function shouldToastNotice(notice: DaemonSessionNotice): boolean { return ( notice.category === 'validation' || @@ -1817,11 +1811,9 @@ export function App({ (error: unknown, fallback: string) => { if (isAbortError(error)) return; if (isDaemonTurnError(error)) { - console.debug('[web-shell] turn error rendered in transcript', error); return; } if (isAlreadyDispatched(error)) { - console.debug('[web-shell] error already handled by notice', error); return; } const message = formatError(error, fallback); @@ -1913,13 +1905,10 @@ export function App({ ); useEffect(() => { - logSessionNoticesHook(notices); for (const notice of notices) { if (shouldToastNotice(notice)) { pushToast(toastToneFromNotice(notice), notice.message); - } else if (notice.category === 'lifecycle') { - console.debug('[web-shell] daemon notice', notice); - } else { + } else if (notice.category !== 'lifecycle') { console.warn('[web-shell] daemon notice', notice); } dismissNotice(notice.id); diff --git a/packages/web-shell/client/components/ChatEditor.tsx b/packages/web-shell/client/components/ChatEditor.tsx index 5761b8d485..81315a1e50 100644 --- a/packages/web-shell/client/components/ChatEditor.tsx +++ b/packages/web-shell/client/components/ChatEditor.tsx @@ -704,7 +704,12 @@ function SlashCommandPanel({ } as CSSProperties; return createPortal( -
+
+
{ setModeDropdownOpen(false); @@ -1457,7 +1463,7 @@ export const ChatEditor = memo( ! )} -
+
@@ -1486,6 +1492,7 @@ export const ChatEditor = memo(