新增: playwright e2e 测试基建与 tauri 垫片
invoke 条件垫片使前端可在无 tauri 容器下直连 dev server 做端到端验证
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
else if (t === 'system' && !window.matchMedia('(prefers-color-scheme: dark)').matches)
|
else if (t === 'system' && !window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
document.documentElement.setAttribute('data-theme', 'light');
|
document.documentElement.setAttribute('data-theme', 'light');
|
||||||
</script>
|
</script>
|
||||||
|
<script type="module" src="/src/mocks/tauri-shim.ts"></script>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Generated
+30
@@ -32,6 +32,7 @@
|
|||||||
"@types/dompurify": "^3.2.0",
|
"@types/dompurify": "^3.2.0",
|
||||||
"@types/sortablejs": "^1.15.9",
|
"@types/sortablejs": "^1.15.9",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"playwright": "^1.63.0",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^6.0.7",
|
"vite": "^6.0.7",
|
||||||
"vue-tsc": "^2.2.0"
|
"vue-tsc": "^2.2.0"
|
||||||
@@ -2990,6 +2991,35 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.63.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.63.0.tgz",
|
||||||
|
"integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.63.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.63.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.63.0.tgz",
|
||||||
|
"integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/points-on-curve": {
|
"node_modules/points-on-curve": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/points-on-curve/-/points-on-curve-0.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/points-on-curve/-/points-on-curve-0.2.0.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"dev:restart": "npm run dev:stop && npm run dev",
|
"dev:restart": "npm run dev:stop && npm run dev",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"build:dev": "tauri build --debug --no-bundle"
|
"build:dev": "tauri build --debug --no-bundle"
|
||||||
},
|
},
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"@types/dompurify": "^3.2.0",
|
"@types/dompurify": "^3.2.0",
|
||||||
"@types/sortablejs": "^1.15.9",
|
"@types/sortablejs": "^1.15.9",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"playwright": "^1.63.0",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^6.0.7",
|
"vite": "^6.0.7",
|
||||||
"vue-tsc": "^2.2.0"
|
"vue-tsc": "^2.2.0"
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'playwright/test'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: 'tests/e2e',
|
||||||
|
timeout: 60_000,
|
||||||
|
headless: true,
|
||||||
|
// 单文件单用例,冒烟规模无需并行;减少并发也降低 vite 冷启动抖动导致的偶发失败
|
||||||
|
workers: 1,
|
||||||
|
retries: 0,
|
||||||
|
reporter: [['list']],
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:1420',
|
||||||
|
headless: true,
|
||||||
|
},
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:1420',
|
||||||
|
reuseExistingServer: true,
|
||||||
|
timeout: 120_000,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
//! 浏览器无头测试专用 Tauri IPC 垫片(条件注入)
|
||||||
|
//!
|
||||||
|
//! 为什么存在:纯 vite 浏览器打开(Playwright 无头 chromium)时,@tauri-apps/api 在调用时读
|
||||||
|
//! window.__TAURI_INTERNALS__,浏览器里没有这个对象,同步 TypeError 直接打断启动链致整页白屏。
|
||||||
|
//! 本模块在该对象缺失时注入一份纯内存实现,invoke 按命令名分发到内置只读假数据。
|
||||||
|
//!
|
||||||
|
//! 数据边界:全部返回值是模块内写死的内存常量,不读也不写任何真实数据库
|
||||||
|
//! (生产 devflow.db 与开发 devflow-dev.db 均不接触),无文件系统与网络访问。
|
||||||
|
//!
|
||||||
|
//! 桌面壳影响面:Tauri 壳内 __TAURI_INTERNALS__ 已由宿主注入,整个模块立即返回,
|
||||||
|
//! 桌面应用零行为变化。
|
||||||
|
|
||||||
|
// 对齐 @tauri-apps/api v2.11 实际访问面(mocks.js 官方 mock 同款字段集)
|
||||||
|
interface TauriInternals {
|
||||||
|
invoke(cmd: string, args?: Record<string, unknown>, options?: unknown): Promise<unknown>
|
||||||
|
transformCallback(cb: (data: unknown) => void, once?: boolean): number
|
||||||
|
unregisterCallback(id: number): void
|
||||||
|
runCallback(id: number, data: unknown): void
|
||||||
|
callbacks: Map<number, (data: unknown) => void>
|
||||||
|
convertFileSrc(filePath: string, protocol?: string): string
|
||||||
|
metadata: {
|
||||||
|
currentWindow: { label: string }
|
||||||
|
currentWebview: { windowLabel: string; label: string }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AiConversationSummaryFixture {
|
||||||
|
id: string
|
||||||
|
title: string | null
|
||||||
|
provider_id: string | null
|
||||||
|
model: string | null
|
||||||
|
archived: boolean
|
||||||
|
pinned: boolean
|
||||||
|
prompt_tokens: number
|
||||||
|
completion_tokens: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 会话摘要假数据(新到旧排序,一条置顶,标题区分业务场景便于 UI 截图辨认)
|
||||||
|
const CONVERSATIONS: AiConversationSummaryFixture[] = [
|
||||||
|
{
|
||||||
|
id: 'conv-mock-1',
|
||||||
|
title: '任务看板数据汇总',
|
||||||
|
provider_id: 'prov-mock-1',
|
||||||
|
model: 'mock-model',
|
||||||
|
archived: false,
|
||||||
|
pinned: true,
|
||||||
|
prompt_tokens: 1024,
|
||||||
|
completion_tokens: 512,
|
||||||
|
created_at: '2026-10-01T09:00:00Z',
|
||||||
|
updated_at: '2026-10-01T09:12:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'conv-mock-2',
|
||||||
|
title: '知识库条目提炼',
|
||||||
|
provider_id: 'prov-mock-1',
|
||||||
|
model: 'mock-model',
|
||||||
|
archived: false,
|
||||||
|
pinned: false,
|
||||||
|
prompt_tokens: 640,
|
||||||
|
completion_tokens: 320,
|
||||||
|
created_at: '2026-09-30T15:00:00Z',
|
||||||
|
updated_at: '2026-09-30T15:20:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'conv-mock-3',
|
||||||
|
title: '项目依赖图排查',
|
||||||
|
provider_id: 'prov-mock-1',
|
||||||
|
model: 'mock-model',
|
||||||
|
archived: false,
|
||||||
|
pinned: false,
|
||||||
|
prompt_tokens: 128,
|
||||||
|
completion_tokens: 64,
|
||||||
|
created_at: '2026-09-29T10:00:00Z',
|
||||||
|
updated_at: '2026-09-29T10:05:00Z',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// conv-mock-1 的历史消息。assistant 一条消息携带三个工具调用 + 对应 tool 消息,
|
||||||
|
// 供前端工具卡折叠/展开渲染路径使用;字段结构与后端 ChatMessage 序列化格式对齐
|
||||||
|
// (tool_calls[].function.arguments 是 JSON 字符串,tool 消息靠 tool_call_id 关联)。
|
||||||
|
const CONV1_MESSAGES = [
|
||||||
|
{
|
||||||
|
id: 'msg-mock-u1',
|
||||||
|
role: 'user',
|
||||||
|
content: '汇总一下进行中的任务,并看看相关项目状态',
|
||||||
|
timestamp: 1759274400000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-mock-a1',
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
timestamp: 1759274402000,
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
id: 'tc-mock-1',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'list_tasks', arguments: '{"status":"in_progress"}' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tc-mock-2',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'list_projects', arguments: '{}' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tc-mock-3',
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'count_tasks', arguments: '{"status":"in_progress"}' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-mock-t1',
|
||||||
|
role: 'tool',
|
||||||
|
tool_call_id: 'tc-mock-1',
|
||||||
|
content: '[{"id":"task-mock-1","title":"编写无头测试基建","status":"in_progress"}]',
|
||||||
|
timestamp: 1759274403000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-mock-t2',
|
||||||
|
role: 'tool',
|
||||||
|
tool_call_id: 'tc-mock-2',
|
||||||
|
content: '[{"id":"proj-mock-1","name":"DevFlow","status":"in_progress"}]',
|
||||||
|
timestamp: 1759274404000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-mock-t3',
|
||||||
|
role: 'tool',
|
||||||
|
tool_call_id: 'tc-mock-3',
|
||||||
|
content: '3',
|
||||||
|
timestamp: 1759274405000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-mock-a2',
|
||||||
|
role: 'assistant',
|
||||||
|
content: '进行中的任务共 3 个,均归属 DevFlow 项目。其中「编写无头测试基建」已进入自验阶段,其余两个阻塞在依赖安装上。',
|
||||||
|
model: 'mock-model',
|
||||||
|
timestamp: 1759274410000,
|
||||||
|
prompt_tokens: 128,
|
||||||
|
completion_tokens: 96,
|
||||||
|
prompt_cache_hit_tokens: 0,
|
||||||
|
prompt_cache_miss_tokens: 128,
|
||||||
|
reasoning_tokens: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-mock-u2',
|
||||||
|
role: 'user',
|
||||||
|
content: '把结论整理成三行以内的摘要',
|
||||||
|
timestamp: 1759274420000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-mock-a3',
|
||||||
|
role: 'assistant',
|
||||||
|
content: '1. 进行中任务 3 个,全部属于 DevFlow。\n2. 测试基建项进展最快。\n3. 其余任务等依赖安装完成即可推进。',
|
||||||
|
model: 'mock-model',
|
||||||
|
timestamp: 1759274425000,
|
||||||
|
prompt_tokens: 96,
|
||||||
|
completion_tokens: 48,
|
||||||
|
prompt_cache_hit_tokens: 64,
|
||||||
|
prompt_cache_miss_tokens: 32,
|
||||||
|
reasoning_tokens: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const OTHER_CONV_MESSAGES = [
|
||||||
|
{
|
||||||
|
id: 'msg-mock-b1',
|
||||||
|
role: 'user',
|
||||||
|
content: '这是一条演示用的历史消息',
|
||||||
|
timestamp: 1759180000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-mock-b2',
|
||||||
|
role: 'assistant',
|
||||||
|
content: '收到,这是浏览器测试环境的固定回复。',
|
||||||
|
model: 'mock-model',
|
||||||
|
timestamp: 1759180005000,
|
||||||
|
prompt_tokens: 32,
|
||||||
|
completion_tokens: 16,
|
||||||
|
prompt_cache_hit_tokens: 0,
|
||||||
|
prompt_cache_miss_tokens: 32,
|
||||||
|
reasoning_tokens: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 提供商假数据:api_key 置空,垫片永远只读,发送消息不会真的发起模型调用
|
||||||
|
const PROVIDERS = [
|
||||||
|
{
|
||||||
|
id: 'prov-mock-1',
|
||||||
|
name: 'Mock Provider',
|
||||||
|
provider_type: 'openai_compat',
|
||||||
|
api_key: '',
|
||||||
|
base_url: 'http://localhost.invalid/v1',
|
||||||
|
default_model: 'mock-model',
|
||||||
|
models: '["mock-model"]',
|
||||||
|
model_configs: [
|
||||||
|
{
|
||||||
|
model_id: 'mock-model',
|
||||||
|
enabled: true,
|
||||||
|
modalities: ['text'],
|
||||||
|
capabilities: ['tool_use'],
|
||||||
|
cost_tier: 'low',
|
||||||
|
intelligence: 'standard',
|
||||||
|
weight: 50,
|
||||||
|
context_window: 32768,
|
||||||
|
probe_source: 'default',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
is_default: true,
|
||||||
|
config: null,
|
||||||
|
created_at: '2026-09-01T00:00:00Z',
|
||||||
|
updated_at: '2026-09-01T00:00:00Z',
|
||||||
|
enabled: true,
|
||||||
|
weight: 50,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 技能假数据(斜杠联想列表用,浏览器测试不触发注入)
|
||||||
|
const SKILLS = [
|
||||||
|
{ name: 'mock-skill-a', description: '演示技能 A', argument_hint: '', source: 'skill', path: '' },
|
||||||
|
{ name: 'mock-skill-b', description: '演示技能 B', argument_hint: '', source: 'skill', path: '' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 返回空列表的查询类命令(值集来自全仓 invoke 调用点盘点,覆盖启动链与各页面挂载路径)
|
||||||
|
const LIST_COMMANDS = new Set([
|
||||||
|
'list_projects',
|
||||||
|
'list_deleted_projects',
|
||||||
|
'list_tasks',
|
||||||
|
'list_ideas',
|
||||||
|
'list_idea_evaluations',
|
||||||
|
'list_workflow_executions',
|
||||||
|
'list_project_modules',
|
||||||
|
'list_branches',
|
||||||
|
'list_module_dependencies',
|
||||||
|
'knowledge_list',
|
||||||
|
'knowledge_list_candidates',
|
||||||
|
'knowledge_events',
|
||||||
|
'ai_get_allowed_dirs',
|
||||||
|
'ai_pending_tool_calls',
|
||||||
|
])
|
||||||
|
|
||||||
|
// 返回固定标量的命令
|
||||||
|
const SCALAR_COMMANDS: Record<string, () => unknown> = {
|
||||||
|
count_tasks: () => 0,
|
||||||
|
settings_get: () => null,
|
||||||
|
settings_get_all: () => ({}),
|
||||||
|
settings_set: () => true,
|
||||||
|
settings_delete: () => true,
|
||||||
|
get_plan_execution: () => false,
|
||||||
|
get_data_dir: () => 'C:\\devflow-browser-mock-data',
|
||||||
|
ai_is_generating: () => false,
|
||||||
|
list_tool_executions: () => ({ items: [], total: 0, has_more: false }),
|
||||||
|
ai_list_providers: () => PROVIDERS,
|
||||||
|
ai_list_skills: () => SKILLS,
|
||||||
|
}
|
||||||
|
|
||||||
|
function conversationDetail(conversationId: string) {
|
||||||
|
const messages = conversationId === 'conv-mock-1' ? CONV1_MESSAGES : OTHER_CONV_MESSAGES
|
||||||
|
return {
|
||||||
|
id: conversationId,
|
||||||
|
title: CONVERSATIONS.find(c => c.id === conversationId)?.title ?? null,
|
||||||
|
messages: JSON.stringify(messages),
|
||||||
|
has_more: false,
|
||||||
|
earliest_seq: null,
|
||||||
|
readonly: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令分发表。返回值直接给 Promise.resolve;写入类命令返回 null 即可,
|
||||||
|
// 前端只关心成功与否,浏览器测试不验证写路径。
|
||||||
|
function buildHandlers(): Record<string, (args: Record<string, unknown>) => unknown> {
|
||||||
|
const handlers: Record<string, (args: Record<string, unknown>) => unknown> = {
|
||||||
|
ai_conversation_list: () => CONVERSATIONS,
|
||||||
|
ai_conversation_switch: args => conversationDetail(String(args?.conversationId ?? 'conv-mock-1')),
|
||||||
|
ai_conversation_load_more: () => ({ messages: '[]', has_more: false, earliest_seq: null }),
|
||||||
|
knowledge_get_config: () => ({
|
||||||
|
auto_extract: false,
|
||||||
|
trigger_mode: 'manual_only',
|
||||||
|
min_messages: 10,
|
||||||
|
auto_inject: false,
|
||||||
|
vector_enabled: false,
|
||||||
|
embedding_provider_id: null,
|
||||||
|
embedding_model: null,
|
||||||
|
}),
|
||||||
|
knowledge_get_detail: () => null,
|
||||||
|
}
|
||||||
|
for (const cmd of LIST_COMMANDS) handlers[cmd] = () => []
|
||||||
|
for (const [cmd, value] of Object.entries(SCALAR_COMMANDS)) handlers[cmd] = value
|
||||||
|
return handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
function install(): void {
|
||||||
|
const internals = window as unknown as Record<string, unknown>
|
||||||
|
|
||||||
|
// 壳内已有宿主注入,立即退出保证桌面零影响;防重复注入(热更新场景幂等)
|
||||||
|
if (internals.__TAURI_INTERNALS__) return
|
||||||
|
if (internals.__DF_TAURI_SHIM_INSTALLED__) return
|
||||||
|
internals.__DF_TAURI_SHIM_INSTALLED__ = true
|
||||||
|
|
||||||
|
const handlers = buildHandlers()
|
||||||
|
|
||||||
|
// 回调注册表对齐官方 mocks.js 结构,listen 返回的 unlisten 依赖这组方法
|
||||||
|
const callbacks = new Map<number, (data: unknown) => void>()
|
||||||
|
let callbackSeq = 0
|
||||||
|
|
||||||
|
const tauriInternals: TauriInternals = {
|
||||||
|
invoke(cmd, args) {
|
||||||
|
// 事件插件统一空实现:listen 返回递增 id 充当 eventId,unlisten/emit 无操作。
|
||||||
|
// 真实事件源在浏览器里不存在,注册成功的假监听永远不会被触发,符合只读预期。
|
||||||
|
if (cmd.startsWith('plugin:event|')) return Promise.resolve(++callbackSeq)
|
||||||
|
if (cmd === 'plugin:window|get_all_windows') return Promise.resolve<string[]>([])
|
||||||
|
const handler = handlers[cmd]
|
||||||
|
if (handler) return Promise.resolve(handler(args ?? {}))
|
||||||
|
// 未知命令返回 null:调用方拿到的语义与后端返 NULL 一致,避免引入新异常面
|
||||||
|
return Promise.resolve(null)
|
||||||
|
},
|
||||||
|
transformCallback(cb) {
|
||||||
|
const id = ++callbackSeq
|
||||||
|
if (cb) callbacks.set(id, cb)
|
||||||
|
return id
|
||||||
|
},
|
||||||
|
unregisterCallback(id) {
|
||||||
|
callbacks.delete(id)
|
||||||
|
},
|
||||||
|
runCallback(id, data) {
|
||||||
|
const cb = callbacks.get(id)
|
||||||
|
if (cb) cb(data)
|
||||||
|
},
|
||||||
|
callbacks,
|
||||||
|
convertFileSrc(filePath) {
|
||||||
|
// 浏览器里没有 asset 协议,透传原值,图片场景留空即可
|
||||||
|
return filePath
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
currentWindow: { label: 'main' },
|
||||||
|
currentWebview: { windowLabel: 'main', label: 'main' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
internals.__TAURI_INTERNALS__ = tauriInternals
|
||||||
|
// 事件插件的浏览器侧登记处,listen 返回的 unlisten 回调会先走这里再 invoke unlisten
|
||||||
|
internals.__TAURI_EVENT_PLUGIN_INTERNALS__ = {
|
||||||
|
unregisterListener() {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
install()
|
||||||
|
|
||||||
|
export {}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { test, expect } from 'playwright/test'
|
||||||
|
|
||||||
|
// 冒烟用例:纯浏览器(无 Tauri 壳)打开 vite dev server,
|
||||||
|
// 验证 Tauri IPC 垫片(src/mocks/tauri-shim.ts)能让应用完整挂载并路由不白屏。
|
||||||
|
// 全程只读内存假数据,不接触任何真实数据库。
|
||||||
|
|
||||||
|
// 主路由取样:AI 首页(默认路由)、任务、项目、知识库,覆盖四条典型挂载链
|
||||||
|
const ROUTES = ['/ai-home', '/tasks', '/projects', '/knowledge']
|
||||||
|
|
||||||
|
for (const route of ROUTES) {
|
||||||
|
test(`路由 ${route} 挂载且无未捕获异常`, async ({ page }) => {
|
||||||
|
const pageErrors: string[] = []
|
||||||
|
page.on('pageerror', (err) => {
|
||||||
|
pageErrors.push(`${err.name}: ${err.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(`/#${route}`)
|
||||||
|
|
||||||
|
// app 完成挂载:main.ts 等路由就绪后才 mount,#app 有元素子节点即视为挂载成功
|
||||||
|
await expect(page.locator('#app > *').first()).toBeVisible({ timeout: 20_000 })
|
||||||
|
|
||||||
|
// 白屏判定:挂载壳内应有实质内容(空壳 fallback 文案也会超过 0,取较高阈值)
|
||||||
|
const html = await page.innerHTML('#app')
|
||||||
|
expect(html.length).toBeGreaterThan(200)
|
||||||
|
|
||||||
|
// ResizeObserver 循环超限是浏览器布局抖动的良性告警,与业务逻辑无关,过滤
|
||||||
|
const realErrors = pageErrors.filter(msg => !/ResizeObserver loop/i.test(msg))
|
||||||
|
expect(realErrors, `路由 ${route} 出现未捕获异常: ${realErrors.join(' | ')}`).toEqual([])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('垫片注入生效且假数据链路可用', async ({ page }) => {
|
||||||
|
await page.goto('/#/ai-home')
|
||||||
|
await expect(page.locator('#app > *').first()).toBeVisible({ timeout: 20_000 })
|
||||||
|
// 浏览器里垫片必须已注入(否则所有 invoke 同步 TypeError)
|
||||||
|
const injected = await page.evaluate(() => Boolean((window as unknown as Record<string, unknown>).__TAURI_INTERNALS__))
|
||||||
|
expect(injected).toBe(true)
|
||||||
|
// 假数据链路:经 window 上的垫片 invoke 会话列表命令,验证分发到内存 fixture。
|
||||||
|
// 不 import @tauri-apps/api,page.evaluate 内联脚本解析不了裸模块名
|
||||||
|
const convCount = await page.evaluate(async () => {
|
||||||
|
const internals = (window as unknown as {
|
||||||
|
__TAURI_INTERNALS__: { invoke: (cmd: string) => Promise<unknown> }
|
||||||
|
}).__TAURI_INTERNALS__
|
||||||
|
const list = (await internals.invoke('ai_conversation_list')) as unknown[]
|
||||||
|
return list.length
|
||||||
|
})
|
||||||
|
expect(convCount).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user