- 使用内部 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
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: 获取源码
|
- name: 获取源码
|
||||||
uses: https://github.com/actions/checkout@v4
|
# Runner 所在网络访问 github.com 不稳定;使用 git.crash.work 上的副本(手动从 GitHub 推送更新)。
|
||||||
|
uses: https://git.crash.work/actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: 准备 Node.js
|
# Runner 位于中国大陆,ubuntu-latest 映射到自带 Node 22 的 node:22-bookworm-slim 镜像;
|
||||||
uses: https://github.com/actions/setup-node@v4
|
# 不再用 setup-node 从 GitHub 下载 Node(经常超时),只校验镜像内版本。
|
||||||
with:
|
- name: 确认 Node.js
|
||||||
node-version: "22"
|
run: |
|
||||||
cache: npm
|
node -v
|
||||||
|
node -e 'if (Number(process.versions.node.split(".")[0]) !== 22) { console.error("需要 Node 22"); process.exit(1) }'
|
||||||
- name: 安装锁定依赖
|
- name: 安装锁定依赖
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: 安装测试浏览器
|
- name: 安装测试浏览器
|
||||||
|
|||||||
@@ -87,6 +87,57 @@ async function freePort() {
|
|||||||
return port;
|
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(
|
before(
|
||||||
async () => {
|
async () => {
|
||||||
// 必须先构建;此处运行 .output,而非开发服务器,确保测试走实际交付入口。
|
// 必须先构建;此处运行 .output,而非开发服务器,确保测试走实际交付入口。
|
||||||
@@ -121,42 +172,7 @@ before(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const port = await freePort();
|
appOrigin = await startApp();
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
browser = await chromium.launch({
|
browser = await chromium.launch({
|
||||||
executablePath: chromePath,
|
executablePath: chromePath,
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ async function bannerGeometry(page) {
|
|||||||
textBottom: text.bottom,
|
textBottom: text.bottom,
|
||||||
topbarTop: bar.top,
|
topbarTop: bar.top,
|
||||||
topbarBottom: bar.bottom,
|
topbarBottom: bar.bottom,
|
||||||
viewport: innerWidth,
|
viewport: document.documentElement.clientWidth,
|
||||||
scrollWidth: document.documentElement.scrollWidth,
|
scrollWidth: document.documentElement.scrollWidth,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -242,10 +242,11 @@ test(
|
|||||||
state.topbarTop >= state.bottom - 0.5,
|
state.topbarTop >= state.bottom - 0.5,
|
||||||
"The full notice and topbar must occupy separate visible regions",
|
"The full notice and topbar must occupy separate visible regions",
|
||||||
);
|
);
|
||||||
assert.equal(
|
// 根元素使用 scrollbar-gutter: stable;Linux/Windows 的经典滚动条预留槽位后,
|
||||||
state.scrollWidth,
|
// 内容宽度会比视口窄 15px,只有 macOS 覆盖式滚动条才会相等。验收只要求不产生横向溢出。
|
||||||
state.viewport,
|
assert.ok(
|
||||||
"A maintenance notice must not widen the document",
|
state.scrollWidth <= state.viewport,
|
||||||
|
`A maintenance notice must not widen the document: ${state.scrollWidth} > ${state.viewport}`,
|
||||||
);
|
);
|
||||||
sizes.push({ width, noticeFits: true, headerBelowNotice: true });
|
sizes.push({ width, noticeFits: true, headerBelowNotice: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ async function enterMonitor(page) {
|
|||||||
await login(page, "/servers/server?id=5");
|
await login(page, "/servers/server?id=5");
|
||||||
await page.getByRole("tab", { name: "监控", exact: true }).waitFor();
|
await page.getByRole("tab", { name: "监控", exact: true }).waitFor();
|
||||||
await page.evaluate(() => document.fonts.ready);
|
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 page.getByRole("tab", { name: "监控", exact: true }).click();
|
||||||
await eventually(
|
await eventually(
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user