/** * # 脚本 前台浏览器回归测试(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);