fix: i18n 链接体系重构 + 五站差异化 toast 皮肤
语言切换/LOGO 同源 bug:手拼 /\${locale} 前缀在默认语言 en 下
产生空 href(<Link href=""> 点击无效、push('') 停留原地)或 /en/*(middleware 契约 404)。
- 新增 lib/locale-href.ts 统一拼接:剥已有语言段、重拼目标语言、根路径保证非空
- language-switcher:/zh 首页切回 en 的 push('') 修复(next-intl 路由
的 {locale} 选项会强制加前缀 forcePrefix,与 as-needed 契约冲突,弃用)
- theme-header/footer/section:LOGO、View all、页脚链接走 localeHref;
isActive 改用无前缀 pathname(en 下高亮失效修复)
- login/signup/email-verified-guard/social 回跳:消除 /en/* 404 死链
- ai-stack/findai 首页搜索表单 action 同修
- 新增 components/site-toaster.tsx:按 NEXT_PUBLIC_THEME_ID 切换
位置/圆角/字体/图标/配色五套皮肤(蓝右下/橙顶中/纸感底中/终端左下/玻璃右上)
- 新增 ui-test-i18n-toast.mjs 回归脚本(16 项浏览器检查全通过)
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* # 脚本 ui-test-i18n-toast.mjs
|
||||
* 说明:i18n 链接修复回归测试 + 各站差异化 toast 验证。
|
||||
* 覆盖:en↔zh 互切(含 /zh 首页回 en 的历史 bug)、子路径切换、
|
||||
* LOGO 回首页(en/zh)、导航高亮、登录默认去向(含 redirect)、
|
||||
* 5 站未登录投票触发 toast 并截图(/tmp/ui-test/toast-<site>.png)。
|
||||
* 运行:bun packages/core/scripts/ui-test-i18n-toast.mjs
|
||||
*/
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const BASE = "https://ai-stack-bdev.opentest.cc";
|
||||
const SITES = [
|
||||
["ai-stack", "https://ai-stack-bdev.opentest.cc", "/tools/chatgpt"],
|
||||
["launchai", "https://launchai-bdev.opentest.cc", "/tools/perplexity"],
|
||||
["the-ai-ledger", "https://the-ai-ledger-bdev.opentest.cc", "/tools/midjourney"],
|
||||
["devai-tools", "https://devai-tools-bdev.opentest.cc", "/tools/github-copilot"],
|
||||
["findai", "https://findai-bdev.opentest.cc", "/tools/figma-ai"],
|
||||
];
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: "/opt/brave.com/brave/brave",
|
||||
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
||||
});
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
page.setDefaultTimeout(30000);
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
async function step(name, fn) {
|
||||
try {
|
||||
await fn();
|
||||
pass++;
|
||||
console.log(` ✓ ${name}`);
|
||||
} catch (e) {
|
||||
fail++;
|
||||
console.log(` ✗ ${name}: ${String(e).split("\n")[0].slice(0, 160)}`);
|
||||
await page.screenshot({ path: `/tmp/ui-test/i18n-fail-${fail}.png` }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/* goto + 网络空闲(水合后才能点导航) */
|
||||
const gotoWait = async (url) => {
|
||||
try {
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
} catch (e) {
|
||||
if (!String(e).includes("ERR_NETWORK_CHANGED")) throw e;
|
||||
await page.waitForTimeout(2000);
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
}
|
||||
await page.waitForLoadState("networkidle", { timeout: 30000 }).catch(() => {});
|
||||
await page.waitForTimeout(600);
|
||||
};
|
||||
|
||||
/* 语言切换:等 get-session 解析 + 菜单出现 */
|
||||
async function switchTo(target) {
|
||||
await page.request.get(`${new URL(page.url()).origin}/api/auth/get-session`).catch(() => {});
|
||||
const btn = page.locator('button[aria-label="Select language"]');
|
||||
await btn.click();
|
||||
await page.getByRole("menuitem", { name: target }).click();
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
|
||||
const langBtn = () => page.locator('button[aria-label="Select language"]');
|
||||
const logoLink = () => page.locator("header a").filter({ has: page.locator('img[src="/icon.svg"]') }).first();
|
||||
|
||||
console.log("== 1. 语言切换(ai-stack)==");
|
||||
await step("en 首页 → 切中文 → /zh", async () => {
|
||||
await gotoWait(`${BASE}/`);
|
||||
await switchTo("中文");
|
||||
if (new URL(page.url()).pathname !== "/zh") throw new Error(`URL=${page.url()}`);
|
||||
});
|
||||
await step("/zh 首页 → 切回英文 → /(原 bug:停留原地)", async () => {
|
||||
await switchTo("English");
|
||||
if (new URL(page.url()).pathname !== "/") throw new Error(`URL=${page.url()}`);
|
||||
});
|
||||
await step("/zh/tools → 切英文 → /tools(子路径)", async () => {
|
||||
await gotoWait(`${BASE}/zh/tools`);
|
||||
await switchTo("English");
|
||||
if (new URL(page.url()).pathname !== "/tools") throw new Error(`URL=${page.url()}`);
|
||||
});
|
||||
await step("/tools → 切中文 → /zh/tools(子路径反向)", async () => {
|
||||
await switchTo("中文");
|
||||
if (new URL(page.url()).pathname !== "/zh/tools") throw new Error(`URL=${page.url()}`);
|
||||
});
|
||||
|
||||
console.log("== 2. LOGO 回首页 ==");
|
||||
await step("en:/tools 点 LOGO → /", async () => {
|
||||
await gotoWait(`${BASE}/tools`);
|
||||
await logoLink().click();
|
||||
await page.waitForTimeout(1200);
|
||||
if (new URL(page.url()).pathname !== "/") throw new Error(`URL=${page.url()}`);
|
||||
});
|
||||
await step("zh:/zh/tools 点 LOGO → /zh", async () => {
|
||||
await gotoWait(`${BASE}/zh/tools`);
|
||||
await logoLink().click();
|
||||
await page.waitForTimeout(1200);
|
||||
if (new URL(page.url()).pathname !== "/zh") throw new Error(`URL=${page.url()}`);
|
||||
});
|
||||
await step("en:/tools 导航项 Discover 有激活高亮", async () => {
|
||||
await gotoWait(`${BASE}/tools`);
|
||||
const active = await page
|
||||
.locator('header nav a[href="/tools"]')
|
||||
.first()
|
||||
.evaluate((el) => el.className.includes("bg-secondary"));
|
||||
if (!active) throw new Error("无激活样式");
|
||||
});
|
||||
|
||||
console.log("== 3. 登录跳转 ==");
|
||||
await step("清理会话(API 登出)", async () => {
|
||||
await page.request.post(`${BASE}/api/auth/sign-out`);
|
||||
const session = await page.request.get(`${BASE}/api/auth/get-session`);
|
||||
if ((JSON.parse(await session.text()) || {}).user) throw new Error("登出失败");
|
||||
});
|
||||
await step("en 带 redirect 登录 → /dashboard(原 bug:/en/ 404)", async () => {
|
||||
await gotoWait(`${BASE}/login?redirect=%2Fdashboard`);
|
||||
await page.getByLabel(/email/i).first().fill("alice@demo.dev");
|
||||
await page.getByLabel(/password/i).first().fill("password123");
|
||||
await page.getByRole("button", { name: /sign in/i }).first().click();
|
||||
await page.waitForURL("**/dashboard", { timeout: 20000 });
|
||||
if (new URL(page.url()).pathname !== "/dashboard") throw new Error(`URL=${page.url()}`);
|
||||
});
|
||||
await step("en 无 redirect 登录默认去当前语言首页 /(不再 /en/ 404)", async () => {
|
||||
await page.request.post(`${BASE}/api/auth/sign-out`);
|
||||
await gotoWait(`${BASE}/login`);
|
||||
await page.getByLabel(/email/i).first().fill("alice@demo.dev");
|
||||
await page.getByLabel(/password/i).first().fill("password123");
|
||||
await page.getByRole("button", { name: /sign in/i }).first().click();
|
||||
await page.waitForURL((u) => !u.pathname.includes("/login"), { timeout: 20000 });
|
||||
if (new URL(page.url()).pathname !== "/") throw new Error(`URL=${page.url()}`);
|
||||
});
|
||||
await step("zh 带 redirect 登录 → /zh/dashboard", async () => {
|
||||
await page.request.post(`${BASE}/api/auth/sign-out`);
|
||||
await gotoWait(`${BASE}/zh/login?redirect=%2Fdashboard`);
|
||||
await page.getByLabel(/邮箱|email/i).first().fill("alice@demo.dev");
|
||||
await page.getByLabel(/密码|password/i).first().fill("password123");
|
||||
// 精确匹配"登录",避免误点"使用 GitHub 登录"
|
||||
await page.getByRole("button", { name: "登录", exact: true }).first().click();
|
||||
await page.waitForURL("**/zh/dashboard", { timeout: 20000 });
|
||||
if (new URL(page.url()).pathname !== "/zh/dashboard") throw new Error(`URL=${page.url()}`);
|
||||
});
|
||||
|
||||
console.log("== 4. 各站差异化 toast(未登录点 Upvote)==");
|
||||
await ctx.clearCookies();
|
||||
const _sess = await page.request.get(`${BASE}/api/auth/get-session`);
|
||||
if ((JSON.parse(await _sess.text()) || {}).user) throw new Error("清理 cookie 后仍有会话");
|
||||
for (const [site, base, tool] of SITES) {
|
||||
await step(`${site}: toast 弹出且皮肤存在`, async () => {
|
||||
await gotoWait(`${base}${tool}`);
|
||||
const voteBtn = page.locator('button:has-text("Upvote"), button:has-text("Upvoted")').first();
|
||||
await voteBtn.waitFor();
|
||||
await voteBtn.click();
|
||||
try {
|
||||
await page.waitForSelector("[data-sonner-toast]", { timeout: 4000 });
|
||||
} catch {
|
||||
// 若已是已投状态,第一次点击是取消投票(无 toast),再点一次触发拦截提示
|
||||
await voteBtn.click();
|
||||
await page.waitForSelector("[data-sonner-toast]", { timeout: 10000 });
|
||||
}
|
||||
await page.waitForTimeout(600);
|
||||
await page.screenshot({ path: `/tmp/ui-test/toast-${site}.png` });
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\n结果: ${pass} 通过 / ${fail} 失败`);
|
||||
await browser.close();
|
||||
process.exit(fail > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user