/** * # 脚本 ui-test-i18n-toast.mjs * 说明:i18n 链接修复回归测试 + 各站差异化 toast 验证。 * 覆盖:en↔zh 互切(含 /zh 首页回 en 的历史 bug)、子路径切换、 * LOGO 回首页(en/zh)、导航高亮、登录默认去向(含 redirect)、 * 5 站未登录投票触发 toast 并截图(/tmp/ui-test/toast-.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);