- 使用内部 checkout 镜像,并校验运行环境中的 Node.js 22 - 检测预览服务端口冲突并重试,避免并行测试误连其他进程 - 调整布局测试的视口宽度和溢出断言,兼容经典滚动条 - 增加时钟暂停的时间余量,避免 CI 调度延迟导致测试失败
This commit is contained in:
4 files changed
+69
-48
No files matched your search
@@ -10,14 +10,16 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: 获取源码
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
# Runner 所在网络访问 github.com 不稳定;使用 git.crash.work 上的副本(手动从 GitHub 推送更新)。
|
||||
uses: https://git.crash.work/actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: 准备 Node.js
|
||||
uses: https://github.com/actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
# Runner 位于中国大陆,ubuntu-latest 映射到自带 Node 22 的 node:22-bookworm-slim 镜像;
|
||||
# 不再用 setup-node 从 GitHub 下载 Node(经常超时),只校验镜像内版本。
|
||||
- name: 确认 Node.js
|
||||
run: |
|
||||
node -v
|
||||
node -e 'if (Number(process.versions.node.split(".")[0]) !== 22) { console.error("需要 Node 22"); process.exit(1) }'
|
||||
- name: 安装锁定依赖
|
||||
run: npm ci
|
||||
- name: 安装测试浏览器
|
||||
|
||||
@@ -87,6 +87,57 @@ async function freePort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
// node --test 并行运行各验收文件:freePort 释放端口到预览进程绑定之间,其他文件的进程可能占用同一端口。
|
||||
// Nitro 绑定失败时只记录 EADDRINUSE 而不退出,HTTP 探测还会命中别的文件的预览服务,
|
||||
// 因此只以本进程输出的监听地址为准,冲突时换端口重试。
|
||||
async function startApp(attempts = 5) {
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
const port = await freePort();
|
||||
const origin = `http://127.0.0.1:${port}`;
|
||||
const app = startProcess(
|
||||
[join(appRoot, ".output", "server", "index.mjs")],
|
||||
{
|
||||
cwd: appRoot,
|
||||
env: {
|
||||
// 使用生产已有的公开 API 配置接入外部边界,不读取或修改项目 .env。
|
||||
...process.env,
|
||||
NODE_ENV: "production",
|
||||
HOST: "127.0.0.1",
|
||||
PORT: String(port),
|
||||
NITRO_HOST: "127.0.0.1",
|
||||
NITRO_PORT: String(port),
|
||||
NUXT_PUBLIC_API_BASE: backendOrigin,
|
||||
},
|
||||
},
|
||||
);
|
||||
const state = await eventually(
|
||||
async () => {
|
||||
if (app.exitCode !== null || app.signalCode !== null) return "exited";
|
||||
if (app.log.includes("EADDRINUSE")) return "collision";
|
||||
if (!app.log.includes(`Listening on ${origin}`)) return false;
|
||||
try {
|
||||
const response = await fetch(`${origin}/login`, {
|
||||
signal: AbortSignal.timeout(1000),
|
||||
});
|
||||
return response.ok && "ready";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
Boolean,
|
||||
"Built app did not start",
|
||||
30000,
|
||||
);
|
||||
if (state === "ready") return origin;
|
||||
if (state === "exited") throw new Error(`Preview exited: ${app.log}`);
|
||||
children.splice(children.indexOf(app), 1);
|
||||
const exited = new Promise((resolve_) => app.once("exit", resolve_));
|
||||
app.kill("SIGKILL");
|
||||
await exited;
|
||||
}
|
||||
throw new Error(`Built app could not bind a free port in ${attempts} tries`);
|
||||
}
|
||||
|
||||
before(
|
||||
async () => {
|
||||
// 必须先构建;此处运行 .output,而非开发服务器,确保测试走实际交付入口。
|
||||
@@ -121,42 +172,7 @@ before(
|
||||
);
|
||||
});
|
||||
|
||||
const port = await freePort();
|
||||
appOrigin = `http://127.0.0.1:${port}`;
|
||||
const app = startProcess(
|
||||
[join(appRoot, ".output", "server", "index.mjs")],
|
||||
{
|
||||
cwd: appRoot,
|
||||
env: {
|
||||
// 使用生产已有的公开 API 配置接入外部边界,不读取或修改项目 .env。
|
||||
...process.env,
|
||||
NODE_ENV: "production",
|
||||
HOST: "127.0.0.1",
|
||||
PORT: String(port),
|
||||
NITRO_HOST: "127.0.0.1",
|
||||
NITRO_PORT: String(port),
|
||||
NUXT_PUBLIC_API_BASE: backendOrigin,
|
||||
},
|
||||
},
|
||||
);
|
||||
await eventually(
|
||||
async () => {
|
||||
if (app.exitCode !== null)
|
||||
throw new Error(`Preview exited: ${app.log}`);
|
||||
try {
|
||||
return (
|
||||
await fetch(`${appOrigin}/login`, {
|
||||
signal: AbortSignal.timeout(1000),
|
||||
})
|
||||
).ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
Boolean,
|
||||
"Built app did not start",
|
||||
30000,
|
||||
);
|
||||
appOrigin = await startApp();
|
||||
|
||||
browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
|
||||
@@ -215,7 +215,7 @@ async function bannerGeometry(page) {
|
||||
textBottom: text.bottom,
|
||||
topbarTop: bar.top,
|
||||
topbarBottom: bar.bottom,
|
||||
viewport: innerWidth,
|
||||
viewport: document.documentElement.clientWidth,
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
};
|
||||
});
|
||||
@@ -242,10 +242,11 @@ test(
|
||||
state.topbarTop >= state.bottom - 0.5,
|
||||
"The full notice and topbar must occupy separate visible regions",
|
||||
);
|
||||
assert.equal(
|
||||
state.scrollWidth,
|
||||
state.viewport,
|
||||
"A maintenance notice must not widen the document",
|
||||
// 根元素使用 scrollbar-gutter: stable;Linux/Windows 的经典滚动条预留槽位后,
|
||||
// 内容宽度会比视口窄 15px,只有 macOS 覆盖式滚动条才会相等。验收只要求不产生横向溢出。
|
||||
assert.ok(
|
||||
state.scrollWidth <= state.viewport,
|
||||
`A maintenance notice must not widen the document: ${state.scrollWidth} > ${state.viewport}`,
|
||||
);
|
||||
sizes.push({ width, noticeFits: true, headerBelowNotice: true });
|
||||
}
|
||||
|
||||
@@ -88,7 +88,9 @@ async function enterMonitor(page) {
|
||||
await login(page, "/servers/server?id=5");
|
||||
await page.getByRole("tab", { name: "监控", exact: true }).waitFor();
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.clock.pauseAt(await page.evaluate(() => Date.now() + 50));
|
||||
// pauseAt 是跳转而非真实等待;只留 50ms 时,读时间与暂停之间的调度延迟(并行验收或 CI)
|
||||
// 会让目标落在过去并报 “Cannot fast-forward to the past”,因此留出 1 秒余量。
|
||||
await page.clock.pauseAt(await page.evaluate(() => Date.now() + 1000));
|
||||
await page.getByRole("tab", { name: "监控", exact: true }).click();
|
||||
await eventually(
|
||||
async () => {
|
||||
|
||||
Reference in new issue
Block a user