Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
xyw_asset_console
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
DaiJiezhang
xyw_asset_console
Commits
06e7d9f9
Commit
06e7d9f9
authored
Jul 27, 2026
by
DaiJiezhang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
test: add vite lifecycle regression coverage
parent
7c16c06a
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
209 additions
and
0 deletions
+209
-0
verification.md
openspec/changes/migrate-vite-build/verification.md
+32
-0
playwright.config.js
playwright.config.js
+19
-0
vite-lifecycle.spec.js
tests/vite-lifecycle.spec.js
+158
-0
No files found.
openspec/changes/migrate-vite-build/verification.md
0 → 100644
View file @
06e7d9f9
# Vite 迁移闭环验证记录
## 自动验证证据
| 需求/任务 | 验证方式 | 实际证据 | 状态 |
|---|---|---|---|
| 单一模块渲染责任(3.3a、3.3b) | Playwright Chromium,直达与刷新 | 修复前
`#/wechat`
、
`#/phone-card`
的初始列表 GET 均为 2;修复后每个场景为 1 | 通过 |
| 路由切换生命周期(3.3c、5.2b) |
`npm run test:e2e -- --reporter=line`
| 2026-07-26:13/13 通过,覆盖两个目标页的直达、刷新、四个来源路由切换,以及替换 pageBody 后的行点击/抽屉关闭 | 通过 |
| 自动测试独立于本机后端(4.2) | Playwright route 拦截 | 两个列表 GET 均返回受控
`{ code: 200, message: "ok", data: [] }`
,测试执行无后端代理错误 | 通过 |
| Vite 构建(4.3) |
`npm run build`
| 2026-07-26:退出码 0;存在第三方
`@vueuse/core`
注释和产物体积警告,未阻断构建 | 通过 |
| 五路由真实联调(5.1、5.2a) | 本机后端 8888 + Vite + Browser Network | 本地 Spring Boot 于 2026-07-26 启动后未监听 8888,未执行 | 阻塞 |
|
`VITE-SMOKE-`
写入、编辑、按 ID 清理(5.3) | 本机后端 API | 因本地后端未监听 8888,未创建任何测试数据 | 阻塞 |
## 审查整改
-
2026-07-26:新增断言,要求被拦截的列表请求保持 GET;补齐 Playwright 业务回调的新手注释;新增替换 DOM 的事件回归。
-
变异检查:临时移除
`RouterView.mounted()`
的
`preparePageShell()`
调用后,替换 DOM 事件用例超时失败;随后已恢复源码。
-
依赖安全审计:
`npm audit fix --package-lock-only`
将间接依赖
`postcss`
从
`8.5.16`
升至
`8.5.23`
、
`nanoid`
升至
`3.3.16`
;随后
`npm ci`
与
`npm audit`
均报告 0 个漏洞。
## 执行环境
-
Node:项目要求
`>=20.19.0 || >=22.12.0`
-
浏览器:Playwright Chromium(本机安装)
-
前端:Vite
`5173`
;后端真实联调端口:
`8888`
## 后续人工验证与清理路径
1.
启动
`./start-backend-jdk17.bat`
与
`npm run dev`
。
2.
分别访问并刷新
`#/overview`
、
`#/domain`
、
`#/wechat`
、
`#/phone-card`
、
`#/alerts`
,记录 Console 与 Network。
3.
仅创建名称或备注以
`VITE-SMOKE-`
开头的手机号卡;记录返回 ID。
4.
编辑并刷新确认可见后,仅调用该 ID 的删除操作;确认列表或按 ID 查询不再返回该记录。
5.
若删除失败,记录 ID、接口响应与人工清理路径;不得删除非本轮创建的数据。
\ No newline at end of file
playwright.config.js
0 → 100644
View file @
06e7d9f9
import
{
defineConfig
}
from
'@playwright/test'
;
/**
* 代码作用(白话):让浏览器回归测试自动启动本地 Vite,并固定使用 Chromium 与 tests 目录;关联文件:F:/Project/xyw_console/package.json、F:/Project/xyw_console/tests/vite-lifecycle.spec.js;关联逻辑(调用链/消息链/数据流):npm run test:e2e -> Playwright 配置 -> Vite :5173 -> 生命周期测试。
*/
export
default
defineConfig
({
testDir
:
'./tests'
,
timeout
:
30
_000
,
use
:
{
baseURL
:
'http://127.0.0.1:5173'
,
browserName
:
'chromium'
},
webServer
:
{
command
:
'npm run dev -- --host 127.0.0.1'
,
url
:
'http://127.0.0.1:5173'
,
reuseExistingServer
:
false
,
timeout
:
30
_000
}
});
tests/vite-lifecycle.spec.js
0 → 100644
View file @
06e7d9f9
import
{
expect
,
test
}
from
'@playwright/test'
;
const
targetPages
=
[
{
module
:
'wechat'
,
hash
:
'#/wechat'
,
requestPath
:
'/api/wechat-records'
,
rootSelector
:
'#wechatListRoot .table-area'
},
{
module
:
'phone'
,
hash
:
'#/phone-card'
,
requestPath
:
'/api/wx-phones'
,
rootSelector
:
'#phoneListRoot .table-area'
}
];
const
sourcePages
=
[
'#/overview'
,
'#/domain'
,
'#/alerts'
,
'#/phone-card'
];
/**
* 代码作用(白话):为每个测试页面注入仅测试使用的 Runtime 挂载计数容器,使测试能观察真实页面是否重复挂载;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/tests/vite-lifecycle.spec.js;关联逻辑(调用链/消息链/数据流):Playwright addInitScript -> 页面启动 -> app-v2.js 读取计数容器 -> 测试断言。
*/
async
function
installMountCounters
(
page
)
{
/**
* 代码作用(白话):在业务脚本执行前建立测试计数对象,让生产代码仅在该对象存在时上报挂载次数;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):addInitScript 回调 -> window.__viteLifecycleCounters -> recordRuntimeMount() -> readMountCount()。
*/
await
page
.
addInitScript
(()
=>
{
window
.
__viteLifecycleCounters
=
{
mounts
:
{
wechat
:
0
,
phone
:
0
}
};
});
}
/**
* 代码作用(白话):拦截两个列表请求并返回与真实接口一致的成功空列表,只统计当前目标接口,隔离本机后端数据但保留页面真实渲染流程;关联文件:F:/Project/xyw_console/src/modules/shared/wechat-api-client.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):页面 Runtime -> fetch 相对 /api 地址 -> Playwright route -> 成功 JSON -> 列表根节点。
*/
async
function
mockInitialListRequest
(
page
,
requestPath
)
{
let
requestCount
=
0
;
const
unexpectedMethods
=
[];
for
(
const
listPath
of
[
'/api/wechat-records'
,
'/api/wx-phones'
])
{
/**
* 代码作用(白话):拦截单个列表接口,记录其 HTTP 方法,并返回受控空列表;关联文件:F:/Project/xyw_console/src/modules/shared/wechat-api-client.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):浏览器 fetch -> route 回调 -> 方法检查/目标计数 -> fulfill JSON -> Runtime 列表渲染。
*/
await
page
.
route
(
`**
${
listPath
}
`
,
async
(
route
)
=>
{
const
method
=
route
.
request
().
method
();
if
(
method
!==
'GET'
)
{
unexpectedMethods
.
push
({
listPath
,
method
});
}
if
(
listPath
===
requestPath
)
{
requestCount
+=
1
;
}
await
route
.
fulfill
({
status
:
200
,
contentType
:
'application/json'
,
body
:
JSON
.
stringify
({
code
:
200
,
message
:
'ok'
,
data
:
[]
})
});
});
}
return
{
/**
* 代码作用(白话):返回当前目标列表的初始请求次数;关联文件:F:/Project/xyw_console/tests/vite-lifecycle.spec.js;关联逻辑(调用链/消息链/数据流):route 回调累加 -> count() -> 每个生命周期场景断言。
*/
count
:
()
=>
requestCount
,
/**
* 代码作用(白话):确认被拦截的列表请求没有从既有 GET 契约变成其他方法;关联文件:F:/Project/xyw_console/src/modules/shared/wechat-api-client.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):route 回调记录非 GET -> assertOnlyGet() -> 生命周期场景失败或通过。
*/
assertOnlyGet
:
()
=>
expect
(
unexpectedMethods
).
toEqual
([]),
/**
* 代码作用(白话):在刷新前清除上一轮页面加载的目标请求计数,单独验证刷新这一轮;关联文件:F:/Project/xyw_console/tests/vite-lifecycle.spec.js;关联逻辑(调用链/消息链/数据流):首次加载 -> reset() -> page.reload() -> count()。
*/
reset
:
()
=>
{
requestCount
=
0
;
}
};
}
/**
* 代码作用(白话):读取页面在测试容器中记录的目标 Runtime 挂载次数,直接验证真实挂载结果;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/tests/vite-lifecycle.spec.js;关联逻辑(调用链/消息链/数据流):renderExternalRuntime() -> 测试计数容器 -> readMountCount() -> 断言。
*/
async
function
readMountCount
(
page
,
module
)
{
/**
* 代码作用(白话):在浏览器页面里取出指定模块的测试挂载计数,缺失时按 0 处理;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):page.evaluate 回调 -> window.__viteLifecycleCounters -> Playwright 期望值。
*/
return
page
.
evaluate
((
moduleKey
)
=>
window
.
__viteLifecycleCounters
?.
mounts
?.[
moduleKey
]
??
0
,
module
);
}
for
(
const
target
of
targetPages
)
{
/**
* 代码作用(白话):验证直接打开目标 Hash 页只会挂载一次 Runtime、读取一次 GET 列表;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):page.goto -> RouterView.mounted() -> renderModule() -> Runtime/列表请求 -> 断言。
*/
test
(
`
${
target
.
module
}
direct entry mounts once and requests its list once`
,
async
({
page
})
=>
{
await
installMountCounters
(
page
);
const
listRequest
=
await
mockInitialListRequest
(
page
,
target
.
requestPath
);
await
page
.
goto
(
`/
${
target
.
hash
}
`
);
await
expect
(
page
.
locator
(
target
.
rootSelector
)).
toBeVisible
();
listRequest
.
assertOnlyGet
();
expect
(
listRequest
.
count
()).
toBe
(
1
);
expect
(
await
readMountCount
(
page
,
target
.
module
)).
toBe
(
1
);
});
/**
* 代码作用(白话):验证刷新目标 Hash 页后,新的一轮页面加载仍只有一次 Runtime 和一次 GET;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):首次加载 -> reset -> page.reload -> RouterView.mounted() -> Runtime/列表请求 -> 断言。
*/
test
(
`
${
target
.
module
}
refresh mounts once and requests its list once`
,
async
({
page
})
=>
{
await
installMountCounters
(
page
);
const
listRequest
=
await
mockInitialListRequest
(
page
,
target
.
requestPath
);
await
page
.
goto
(
`/
${
target
.
hash
}
`
);
await
expect
(
page
.
locator
(
target
.
rootSelector
)).
toBeVisible
();
listRequest
.
reset
();
await
page
.
reload
();
await
expect
(
page
.
locator
(
target
.
rootSelector
)).
toBeVisible
();
listRequest
.
assertOnlyGet
();
expect
(
listRequest
.
count
()).
toBe
(
1
);
expect
(
await
readMountCount
(
page
,
target
.
module
)).
toBe
(
1
);
});
}
for
(
const
sourceHash
of
sourcePages
)
{
/**
* 代码作用(白话):验证从每个其余 Hash 路由切到企微页时只有一次目标 Runtime 和 GET;关联文件:F:/Project/xyw_console/src/router/index.js、F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):来源路由 -> 侧栏点击 -> router.push -> RouterView.mounted -> 企微 Runtime -> 断言。
*/
test
(
`wechat route transition from
${
sourceHash
}
mounts once and requests its list once`
,
async
({
page
})
=>
{
await
installMountCounters
(
page
);
const
listRequest
=
await
mockInitialListRequest
(
page
,
'/api/wechat-records'
);
await
page
.
goto
(
`/
${
sourceHash
}
`
);
await
page
.
locator
(
'[data-module="wechat"]'
).
click
();
await
expect
(
page
.
locator
(
'#wechatListRoot .table-area'
)).
toBeVisible
();
listRequest
.
assertOnlyGet
();
expect
(
listRequest
.
count
()).
toBe
(
1
);
expect
(
await
readMountCount
(
page
,
'wechat'
)).
toBe
(
1
);
});
}
for
(
const
sourceHash
of
[
'#/overview'
,
'#/domain'
,
'#/wechat'
,
'#/alerts'
])
{
/**
* 代码作用(白话):验证从每个其余 Hash 路由切到手机号页时只有一次目标 Runtime 和 GET;关联文件:F:/Project/xyw_console/src/router/index.js、F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):来源路由 -> 侧栏点击 -> router.push -> RouterView.mounted -> 手机号 Runtime -> 断言。
*/
test
(
`phone route transition from
${
sourceHash
}
mounts once and requests its list once`
,
async
({
page
})
=>
{
await
installMountCounters
(
page
);
const
listRequest
=
await
mockInitialListRequest
(
page
,
'/api/wx-phones'
);
await
page
.
goto
(
`/
${
sourceHash
}
`
);
await
page
.
locator
(
'[data-module="phone"]'
).
click
();
await
expect
(
page
.
locator
(
'#phoneListRoot .table-area'
)).
toBeVisible
();
listRequest
.
assertOnlyGet
();
expect
(
listRequest
.
count
()).
toBe
(
1
);
expect
(
await
readMountCount
(
page
,
'phone'
)).
toBe
(
1
);
});
}
/**
* 代码作用(白话):验证路由替换 pageBody 后,新的行点击和抽屉关闭事件仍绑定在新 DOM 上;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):路由切换 -> preparePageShell() -> bindLayoutEvents() -> 行点击打开抽屉 -> 关闭按钮隐藏抽屉。
*/
test
(
'route replacement keeps page body and drawer close events bound'
,
async
({
page
})
=>
{
await
page
.
goto
(
'/#/overview'
);
await
page
.
locator
(
'[data-module="domain"]'
).
click
();
await
page
.
locator
(
'#pageBody [data-record-id][data-source-key]'
).
first
().
click
();
await
expect
(
page
.
locator
(
'#detailDrawer'
)).
not
.
toHaveClass
(
/hidden/
);
await
page
.
locator
(
'#drawerClose'
).
click
();
await
expect
(
page
.
locator
(
'#detailDrawer'
)).
toHaveClass
(
/hidden/
);
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment