// Wave 3.5 auth walkthrough (scratch): 登录(秒进/无邮箱/第三方/切换) + 注册(无邮箱/5字段) + 切换态. import { chromium } from "playwright"; import fs from "node:fs"; import path from "node:path"; const BASE = process.env.BASE || "http://localhost:5173"; const OUT = path.resolve("../../../_qa_shots/wave35"); fs.mkdirSync(OUT, { recursive: true }); const browser = await chromium.launch({ headless: true }); const r = { login: {}, register: {}, switch: {}, consoleErrors: [] }; // 登录态(无 token,真在登录页) { const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const p = await ctx.newPage(); p.on("console", (m) => { if (m.type() === "error") r.consoleErrors.push("login:" + m.text()); }); p.on("pageerror", (e) => r.consoleErrors.push("login:PAGEERR:" + e.message)); await p.goto(BASE + "/login", { waitUntil: "load" }); await p.waitForTimeout(500); r.login = { usernameField: await p.locator("#auth-username").count(), emailField: await p.locator("input[type=email]").count(), ssoButtons: await p.locator(".sso-btn").count(), switchRow: await p.locator(".switch-row").count(), }; await p.screenshot({ path: path.join(OUT, "login.png") }); // 登录秒进(demo 账号) await p.fill("#auth-username", "airshelf"); await p.fill("#auth-pwd", "Restraint2026"); const t0 = Date.now(); await p.click("button.btn-cta"); try { await p.waitForFunction(() => !location.pathname.startsWith("/login"), { timeout: 12000 }); r.login.loginMs = Date.now() - t0; } catch { r.login.loginMs = -1; } await ctx.close(); } // 注册态 { const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const p = await ctx.newPage(); p.on("console", (m) => { if (m.type() === "error") r.consoleErrors.push("reg:" + m.text()); }); p.on("pageerror", (e) => r.consoleErrors.push("reg:PAGEERR:" + e.message)); await p.goto(BASE + "/register", { waitUntil: "load" }); await p.waitForTimeout(600); const bodyText = await p.locator("body").innerText().catch(() => ""); r.register = { emailField: await p.locator("input[type=email]").count(), textInputs: await p.locator(".auth-form input[type=text], .auth-form input[type=password]").count(), hasTeamLabel: /团队名|团队名称/.test(bodyText), hasInvite: /邀请码/.test(bodyText), hasEmailText: /邮箱/.test(bodyText), ctaButton: await p.locator(".btn-cta").count(), switchRow: await p.locator(".switch-row").count(), }; await p.screenshot({ path: path.join(OUT, "register.png") }); await ctx.close(); } // 切换:login → 点切换行 → register { const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const p = await ctx.newPage(); await p.goto(BASE + "/login", { waitUntil: "load" }); await p.waitForTimeout(500); const link = p.locator(".switch-row a, .switch-row button").first(); if (await link.count()) { await link.click(); await p.waitForTimeout(400); r.switch = { urlNowRegister: p.url().includes("/register"), registerFormShown: /邀请码/.test(await p.locator("body").innerText().catch(() => "")), }; await p.screenshot({ path: path.join(OUT, "switched-to-register.png") }); } else r.switch = "no switch link"; await ctx.close(); } await browser.close(); console.log(JSON.stringify(r, null, 2)); fs.writeFileSync(path.join(OUT, "summary.json"), JSON.stringify(r, null, 2));