diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2cbf1909..a8a9088a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,5 @@ jobs: - name: Install Playwright browsers run: pnpm exec playwright install --with-deps chromium - - name: Run gate E2E checks (VCR replay) + - name: Run real UI smoke and gate E2E checks run: pnpm test:gate:e2e - env: - E2E_VCR_MODE: replay diff --git a/package.json b/package.json index f26f0383..31b83600 100644 --- a/package.json +++ b/package.json @@ -34,14 +34,16 @@ "check:extension-release-secrets": "node scripts/check-extension-release-secrets.mjs packages/extension/dist", "test:e2e": "playwright test", "test:e2e:smart": "node scripts/smart-e2e.js", + "test:e2e:ui-smoke": "cross-env E2E_VCR_MODE=live E2E_EMPTY_MODEL_CONFIG=1 node scripts/run-e2e-group.js uiSmoke", "test:e2e:gate": "node scripts/run-e2e-group.js gate", + "test:e2e:gate:replay": "cross-env E2E_VCR_MODE=replay node scripts/run-e2e-group.js gate", "test:e2e:extended": "node scripts/run-e2e-group.js extended", "test:e2e:record": "node scripts/run-e2e-record.js", "test:e2e:replay": "cross-env E2E_VCR_MODE=replay node scripts/run-e2e-group.js extended", "compare:calibrate": "pnpm -F @prompt-optimizer/core build && node scripts/run-structured-compare-calibration.mjs", "test:gate:core": "pnpm -F @prompt-optimizer/core test:gate", "test:gate:ui": "pnpm -F @prompt-optimizer/core build && pnpm -F @prompt-optimizer/ui test", - "test:gate:e2e": "pnpm test:e2e:gate", + "test:gate:e2e": "node scripts/run-many.js -s test:e2e:ui-smoke test:e2e:gate:replay", "test:gate": "node scripts/run-many.js -s test:repo test:gate:core test:gate:ui", "test:gate:full": "node scripts/run-many.js -s test:gate test:gate:e2e", "test:fast": "pnpm -r test --run --passWithNoTests", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 280fe6c7..0c340ed3 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -43,6 +43,7 @@ "preload.js", "remote-storage.js", "config/**/*", + "icons/**/*", "web-dist/**/*", "node_modules/**/*" ], diff --git a/packages/ui/src/components/ModelManager.vue b/packages/ui/src/components/ModelManager.vue index 5ca1e7b0..9eb31e8e 100644 --- a/packages/ui/src/components/ModelManager.vue +++ b/packages/ui/src/components/ModelManager.vue @@ -64,8 +64,15 @@ -
- +
+ diff --git a/packages/ui/src/components/SelectWithConfig.vue b/packages/ui/src/components/SelectWithConfig.vue index 67efae52..d4da7e36 100644 --- a/packages/ui/src/components/SelectWithConfig.vue +++ b/packages/ui/src/components/SelectWithConfig.vue @@ -13,6 +13,7 @@ {{ emptyText || t('model.select.noAvailableModels') }} [key, ''])) + : {}), ...(USE_VCR_PLACEHOLDER_KEYS ? { VITE_SILICONFLOW_API_KEY: process.env.VITE_SILICONFLOW_API_KEY || 'vcr', diff --git a/scripts/e2e-groups.js b/scripts/e2e-groups.js index 45e2f242..e6273ba6 100644 --- a/scripts/e2e-groups.js +++ b/scripts/e2e-groups.js @@ -1,4 +1,7 @@ const groups = { + uiSmoke: [ + 'tests/e2e/regression/model-configuration-smoke.spec.ts', + ], gate: [ 'tests/e2e/workflows/p0-route-smoke.spec.ts', 'tests/e2e/regression/root-route-bootstrap.spec.ts', diff --git a/scripts/package-scripts.test.mjs b/scripts/package-scripts.test.mjs index 64ecd219..9270985c 100644 --- a/scripts/package-scripts.test.mjs +++ b/scripts/package-scripts.test.mjs @@ -90,3 +90,18 @@ test('mcp-server bin points to a file that exists before build output is generat `Expected ${binTargetPath} to exist so pnpm can create the workspace bin shim during install`, ) }) + +test('desktop package includes every runtime window icon', () => { + const desktopPackage = readJson(path.join('packages', 'desktop', 'package.json')) + const packagedFiles = desktopPackage.build?.files ?? [] + + assert.ok( + packagedFiles.includes('icons/**/*'), + 'Desktop build.files must include the icons directory used by main.js at runtime', + ) + + for (const iconName of ['app-icon.ico', 'app-icon.icns', 'app-icon.png']) { + const iconPath = path.join(process.cwd(), 'packages', 'desktop', 'icons', iconName) + assert.equal(fs.existsSync(iconPath), true, `Expected runtime icon ${iconPath} to exist`) + } +}) diff --git a/scripts/pnpm-runner.js b/scripts/pnpm-runner.js new file mode 100644 index 00000000..78be4375 --- /dev/null +++ b/scripts/pnpm-runner.js @@ -0,0 +1,39 @@ +const { spawn, spawnSync } = require('node:child_process') + +function getPnpmRunnerSpec(platform = process.platform, env = process.env) { + if (platform === 'win32') { + return { + command: env.ComSpec || env.COMSPEC || 'cmd.exe', + argsPrefix: ['/d', '/s', '/c', 'pnpm'], + shell: false, + } + } + + return { + command: 'pnpm', + argsPrefix: [], + shell: false, + } +} + +function spawnPnpm(args, options = {}) { + const { command, argsPrefix, shell } = getPnpmRunnerSpec() + return spawn(command, [...argsPrefix, ...args], { + shell, + ...options, + }) +} + +function spawnPnpmSync(args, options = {}) { + const { command, argsPrefix, shell } = getPnpmRunnerSpec() + return spawnSync(command, [...argsPrefix, ...args], { + shell, + ...options, + }) +} + +module.exports = { + getPnpmRunnerSpec, + spawnPnpm, + spawnPnpmSync, +} diff --git a/scripts/run-e2e-group.js b/scripts/run-e2e-group.js index 720dc4be..d96e1273 100644 --- a/scripts/run-e2e-group.js +++ b/scripts/run-e2e-group.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -const { spawnSync } = require('child_process') const { groups } = require('./e2e-groups') +const { spawnPnpmSync } = require('./pnpm-runner') function runGroup(groupName, extraArgs = []) { const specs = groups[groupName] @@ -17,11 +17,9 @@ function runGroup(groupName, extraArgs = []) { } console.log('') - const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' - const result = spawnSync(pnpmCommand, ['exec', 'playwright', 'test', ...specs, ...extraArgs], { + const result = spawnPnpmSync(['exec', 'playwright', 'test', ...specs, ...extraArgs], { stdio: 'inherit', - env: process.env, - shell: process.platform === 'win32' + env: process.env }) if (result.error) { diff --git a/scripts/run-e2e-record.js b/scripts/run-e2e-record.js index ee313abe..e5a7dd22 100644 --- a/scripts/run-e2e-record.js +++ b/scripts/run-e2e-record.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -const { spawnSync } = require('child_process') +const { spawnPnpmSync } = require('./pnpm-runner') const extraArgs = process.argv.slice(2) @@ -10,14 +10,12 @@ if (extraArgs.length === 0) { process.exit(1) } -const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' -const result = spawnSync(pnpmCommand, ['exec', 'playwright', 'test', ...extraArgs], { +const result = spawnPnpmSync(['exec', 'playwright', 'test', ...extraArgs], { stdio: 'inherit', env: { ...process.env, E2E_VCR_MODE: 'record' - }, - shell: process.platform === 'win32' + } }) if (result.error) { diff --git a/scripts/run-many.js b/scripts/run-many.js index f4bca4d2..0b18063f 100644 --- a/scripts/run-many.js +++ b/scripts/run-many.js @@ -1,20 +1,6 @@ #!/usr/bin/env node -const { spawn } = require('node:child_process'); - -function getRunnerSpec(platform = process.platform) { - if (platform === 'win32') { - return { - command: 'pnpm', - shell: true, - }; - } - - return { - command: 'pnpm', - shell: false, - }; -} +const { getPnpmRunnerSpec, spawnPnpm } = require('./pnpm-runner'); function parseArgs(argv) { let parallel = false; @@ -40,10 +26,8 @@ function parseArgs(argv) { function runPackageScript(script, options = {}) { return new Promise((resolve, reject) => { - const { command, shell } = getRunnerSpec(); - const child = spawn(command, ['run', script], { + const child = spawnPnpm(['run', script], { stdio: 'inherit', - shell, env: process.env, ...options, }); @@ -90,7 +74,7 @@ if (require.main === module) { } module.exports = { - getRunnerSpec, + getRunnerSpec: getPnpmRunnerSpec, main, parseArgs, runPackageScript, diff --git a/scripts/run-many.test.mjs b/scripts/run-many.test.mjs index badc19c8..b68265fc 100644 --- a/scripts/run-many.test.mjs +++ b/scripts/run-many.test.mjs @@ -21,13 +21,15 @@ test('parseArgs supports parallel aliases', () => { }); }); -test('getRunnerSpec uses shell mode for Windows pnpm invocation', () => { - assert.deepEqual(getRunnerSpec('win32'), { - command: 'pnpm', - shell: true, +test('getRunnerSpec avoids shell mode while preserving Windows pnpm compatibility', () => { + assert.deepEqual(getRunnerSpec('win32', { ComSpec: 'C:\\Windows\\System32\\cmd.exe' }), { + command: 'C:\\Windows\\System32\\cmd.exe', + argsPrefix: ['/d', '/s', '/c', 'pnpm'], + shell: false, }); assert.deepEqual(getRunnerSpec('linux'), { command: 'pnpm', + argsPrefix: [], shell: false, }); }); diff --git a/tests/e2e/regression/model-configuration-smoke.spec.ts b/tests/e2e/regression/model-configuration-smoke.spec.ts new file mode 100644 index 00000000..edcfa781 --- /dev/null +++ b/tests/e2e/regression/model-configuration-smoke.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from '../fixtures' + +test.describe('Model configuration smoke', () => { + test('first-use model selector opens a manager with switchable text, image, and function tabs', async ({ page }) => { + await page.goto('/') + await expect(page.locator('[data-testid="workspace"][data-mode="basic-system"]')).toBeVisible({ + timeout: 45000, + }) + + const modelSelect = page.getByTestId('basic-system-model-select') + await expect(modelSelect).toBeVisible() + await modelSelect.click() + + const emptyConfigAction = page.getByTestId('select-empty-config-action') + await expect(emptyConfigAction).toBeVisible() + await expect(page.getByText(/暂无可用模型|No available models/)).toBeVisible() + await emptyConfigAction.click() + + const dialog = page.getByTestId('model-manager-dialog') + await expect(dialog).toBeVisible() + + const tabs = page.getByTestId('model-manager-tabs') + const tabItems = tabs.locator('.n-tabs-tab') + await expect(tabs).toContainText(/文本模型|Text Models/) + await expect(tabs).toContainText(/图像模型|Image Models/) + await expect(tabs).toContainText(/功能模型|Function Models/) + + for (const tabLabel of [/图像模型|Image Models/, /功能模型|Function Models/, /文本模型|Text Models/]) { + const tab = tabItems.filter({ hasText: tabLabel }).first() + await tab.click() + await expect(tab).toHaveClass(/n-tabs-tab--active/) + } + }) +})