Files
ai-directory-platform/packages/core/scripts/ui-test-i18n-toast.mjs
T
bigcode b5a4532dd0 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 项浏览器检查全通过)
2026-09-16 11:11:19 +08:00

167 lines
7.3 KiB
JavaScript
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.
/**
* # 脚本 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);