Files
dusuniot/scripts/front-browser-test.ts
T

99 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 脚本 前台浏览器回归测试(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);