v0.13.0: 前台导航修复(loader 服务函数化防客户端退化)、全站图片占位图+懒加载、favicon、分页圆点、导航 1400px 居中、缺失图片补齐脚本
This commit is contained in:
@@ -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);
|
||||
@@ -31,7 +31,10 @@ export default {
|
||||
/** # 函数 请求入口:静态文件优先,其余交给 Start handler */
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const f = staticFiles.get(url.pathname);
|
||||
// 尾斜杠归一化:偶发的 "<file>.jpg/" 形态请求(SPA 预取取回/上游转发)直接按静态
|
||||
// 文件应答,避免交给 Start handler 后落入路由 catch-all 返回 404
|
||||
let f = staticFiles.get(url.pathname);
|
||||
if (!f && url.pathname.endsWith("/")) f = staticFiles.get(url.pathname.replace(/\/+$/, ""));
|
||||
if (f) {
|
||||
return new Response(Bun.file(f), {
|
||||
headers: { "cache-control": "public, max-age=31536000, immutable" },
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* # 脚本 补齐缺失的上传图片(从原站 dusuniot.com 拉取)
|
||||
* 背景:导入另抓的 html/meta 引用了 uploads 里的图片,但部分文件未同步到
|
||||
* public/uploads(如 landing-pages 分类卡片 2026/11 目录)。本脚本:
|
||||
* 1. 扫库(contents.html/meta/excerpt、products 文本列、media.url)收集全部 uploads 引用路径;
|
||||
* 2. 本地缺失的文件从 https://www.dusuniot.com/wp-content/uploads/<path> 下载补齐;
|
||||
* 3. 输出成功/失败清单。可重复执行(幂等,跳过已存在文件)。
|
||||
*/
|
||||
import { mkdirSync, existsSync, writeFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { initDb } from "~/db";
|
||||
import { db } from "~/db";
|
||||
import { contents, products, media } from "~/db/schema";
|
||||
|
||||
const PUBLIC_ROOT = join(process.cwd(), "public", "uploads");
|
||||
const ORIGIN = "https://www.dusuniot.com/wp-content/uploads/";
|
||||
|
||||
await initDb();
|
||||
|
||||
/** # 成员变量 引用路径去重集合(含 html/meta/excerpt/products/media 全部来源) **/
|
||||
const paths = new Set<string>();
|
||||
const addFromText = (s: unknown) => {
|
||||
if (s == null || typeof s !== "string") return;
|
||||
for (const m of s.matchAll(/(?:https?:\/\/(?:www\.)?dusuniot\.com)?\/wp-content\/uploads\/([\w\-./%~()+!@^[\]!'*,]+?\.(?:png|jpe?g|webp|gif|svg|ico|mp4|pdf))/gi)) {
|
||||
paths.add(m[1]);
|
||||
}
|
||||
for (const m of s.matchAll(/\/uploads\/((?:\d{4}\/\d{2}\/)?[\w\-./%~()+!@^[\]!'*,]+?\.(?:png|jpe?g|webp|gif|svg|ico|mp4|pdf))/gi)) {
|
||||
paths.add(m[1]);
|
||||
}
|
||||
};
|
||||
|
||||
// contents:html/meta/excerpt
|
||||
for (const row of await db.select({ html: contents.html, meta: contents.meta, excerpt: contents.excerpt, seoTitle: contents.seoTitle }).from(contents)) {
|
||||
addFromText(row.html);
|
||||
addFromText(row.excerpt);
|
||||
addFromText(row.seoTitle);
|
||||
addFromText(row.meta);
|
||||
}
|
||||
// products:常见文本/jsonb 列
|
||||
for (const row of await db.select().from(products)) {
|
||||
for (const v of Object.values(row as Record<string, unknown>)) addFromText(v);
|
||||
}
|
||||
// media 表登记的 URL
|
||||
for (const row of await db.select({ url: media.url }).from(media)) addFromText(row.url);
|
||||
|
||||
console.log("引用路径总数:", paths.size);
|
||||
|
||||
let ok = 0;
|
||||
let okExisting = 0;
|
||||
const failed: string[] = [];
|
||||
for (const p of [...paths].sort()) {
|
||||
// 解码 WP URL 编码与 query
|
||||
const clean = p.split("?")[0];
|
||||
const local = join(PUBLIC_ROOT, decodeURIComponent(clean));
|
||||
if (existsSync(local)) {
|
||||
okExisting++;
|
||||
continue;
|
||||
}
|
||||
const url = ORIGIN + clean;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
failed.push(`${res.status} ${clean}`);
|
||||
continue;
|
||||
}
|
||||
const buf = new Uint8Array(await res.arrayBuffer());
|
||||
if (buf.byteLength < 100) {
|
||||
failed.push(`DOCSIZE ${clean}`);
|
||||
continue;
|
||||
}
|
||||
mkdirSync(dirname(local), { recursive: true });
|
||||
writeFileSync(local, buf);
|
||||
ok++;
|
||||
} catch (e) {
|
||||
failed.push(`ERR ${clean}: ${(e as Error).message.slice(0, 80)}`);
|
||||
}
|
||||
}
|
||||
console.log(`已存在 ${okExisting},新下载 ${ok},失败 ${failed.length}`);
|
||||
for (const f of failed.slice(0, 40)) console.log(" fail:", f);
|
||||
Reference in New Issue
Block a user