v0.13.0: 前台导航修复(loader 服务函数化防客户端退化)、全站图片占位图+懒加载、favicon、分页圆点、导航 1400px 居中、缺失图片补齐脚本

This commit is contained in:
bigcode
2026-11-04 10:30:16 +08:00
parent 4fba80a216
commit 7fd9e75887
28 changed files with 599 additions and 208 deletions
+98
View File
@@ -0,0 +1,98 @@
/**
* # 脚本 前台浏览器回归测试(playwright + headless chromium)
* 覆盖本轮修复:LOGO 返回首页(Elementor 正常渲染)、/landing-pages/ 卡片图显示、
* 后退历史、分页圆点去除、导航 1400px 居中、favicon、图片占位背景。
* 输出 PASS/FAIL 清单。
*/
import { chromium } from "playwright-core";
import { writeFileSync } from "node:fs";
const CHROME = `${process.env.HOME}/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome`;
const BASE = "http://localhost:3312";
const results: { name: string; ok: boolean; detail?: string }[] = [];
const check = (name: string, ok: boolean, detail?: string) => {
results.push({ name, ok, detail });
console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`);
};
const browser = await chromium.launch({ executablePath: CHROME, headless: true, args: ["--no-sandbox"] });
const ctx = await browser.newContext({ viewport: { width: 1500, height: 900 } });
const page = await ctx.newPage();
const http404: string[] = [];
page.on("response", (r) => { if (r.status() === 404) http404.push(r.url()); });
try {
// 1. logging 首页 SSR
await page.goto(BASE + "/", { waitUntil: "networkidle" });
check("首页 SSR(Elementor hero)", await page.getByText("System on Module").count() > 0 || (await page.locator("main .content-html").count()) > 0);
// 2. favicon link 注入
const icon = await page.locator('link[rel="icon"]').first().getAttribute("href").catch(() => null);
check("favicon link 注入", icon === "/favicon.ico", String(icon));
const iconStatus = await ctx.request.get(BASE + icon || "/favicon.ico");
check("favicon.ico 可访问", iconStatus.status() === 200);
// 3. 导航 1400px 居中
const navBox = await page.locator("header div.flex.h-12, header div.flex.min-h\\[68px\\]").first().boundingBox().catch(() => null);
const navWidth = navBox?.width ?? 0;
check("主导航容器 1400px 居中", Math.abs(navWidth - 1400) < 6, `实测 ${navWidth.toFixed(0)}px`);
// 4. /landing-pages/ 卡片图(无 404 且出现 banner 图)
await page.goto(BASE + "/landing-pages/", { waitUntil: "networkidle" });
const cardImgs = await page.locator("main .ant-table-row img, main img[src*='uploads']").count();
const badImgs = await page.evaluate(() => Array.from(document.querySelectorAll(".ds-thumb img, main img")).filter((i) => (i as HTMLImageElement).naturalWidth === 0 && (i as HTMLImageElement).complete).length);
check("landing-pages 卡片图加载", cardImgs > 4, `图片 ${cardImgs} 张,加载失败 ${badImgs}`);
// 5. 页面无明显 404 请求
check("无 404 静态资源", http404.length === 0, http404.slice(0, 3).join(" | "));
// 6. 点击 LOGO 回首页(客户端导航)→ Elementor 渲染 + 后退正常
await page.click("header a[href='/']");
await page.waitForTimeout(2800);
const homeKids = await page.locator("main > *").count();
check("LOGO 客户端导航回首页", page.url().endsWith("/") && homeKids > 0, `URL=${page.url()} main子节点=${homeKids}`);
writeFileSync("/tmp/rt-home.png", await page.screenshot());
await page.goBack({ waitUntil: "domcontentloaded" });
await page.waitForTimeout(2000);
const backOk = await page.getByText("LANDING PAGES").count() > 0 || await page.getByRole("heading").filter({ hasText: /Landing/i }).count() > 0;
check("后退回 landing 正常", backOk && page.url().includes("landing-pages"), page.url());
// 7. 分页圆点去除(/blog/page/2/ 有分页)
await page.goto(BASE + "/collection/", { waitUntil: "networkidle" });
await page.evaluate(() => { window.scrollTo(0, document.body.scrollHeight); });
await page.waitForTimeout(1500);
const pagNav = await page.locator('nav[aria-label="Pagination"]').first().count();
if (pagNav) {
const text = await page.locator('nav[aria-label="Pagination"]').first().innerText();
check("分页无多余圆点", !/[•●]/.test(text), text.replace(/\n/g, " ").slice(0, 60));
// 分页客户端翻页(Link)
await page.locator('nav[aria-label="Pagination"] a').filter({ hasText: "2" }).first().click().catch(() => {});
await page.waitForTimeout(2200);
check("分页客户端翻页", page.url().includes("page=2"), page.url());
} else {
check("分页存在性", false, "当前页无分页导航(测试面缩窄)");
}
// 8. 图片懒加载属性(全站 img 至少一个 loading=lazy 且占位图 CSS 在)
const lazyStats = await page.evaluate(() => {
const imgs = Array.from(document.querySelectorAll(".ds-site img")) as HTMLImageElement[];
return { total: imgs.length, lazy: imgs.filter((i) => i.getAttribute("loading") === "lazy").length };
});
check("全站图片 loading=lazy 注入", lazyStats.total > 0 && lazyStats.lazy >= 1, `lazy ${lazyStats.lazy}/${lazyStats.total}`);
const bg = await page.evaluate(() => getComputedStyle(document.querySelector(".ds-site img")!).backgroundImage);
check("占位图 CSS 生效", /placeholder\.jpg/.test(bg), bg.slice(0, 80));
// 9. 产品页客户端导航(Link 卡片点开)
await page.goto(BASE + "/shop/", { waitUntil: "networkidle" });
await page.locator("main a[href^='/product/']").first().click();
await page.waitForTimeout(2500);
check("产品页客户端导航", /\/product\/.+/.test(page.url()) && (await page.locator("main .content-html, main img").count()) > 0, page.url());
} catch (e) {
check("脚本异常", false, e instanceof Error ? e.message.slice(0, 300) : String(e));
}
await browser.close();
const fail = results.filter((r) => !r.ok);
console.log(`\n==== ${results.length - fail.length}/${results.length} PASS ====`);
process.exit(0);