import { describe, expect, it } from "vitest"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; const readLoginView = () => readFileSync(resolve(__dirname, "./Login.vue"), "utf8"); describe("Login protocol agreement", () => { it("requires protocol agreement before calling login", () => { const source = readLoginView(); const protocolGuardIndex = source.indexOf("!form.agreeProtocol"); const loginCallIndex = source.indexOf("auth.login(form.email, form.password)"); expect(source).toContain('v-model="form.agreeProtocol"'); expect(source).toContain("我已阅读并同意"); expect(source).toContain("请先阅读并同意用户协议"); expect(protocolGuardIndex).toBeGreaterThan(-1); expect(loginCallIndex).toBeGreaterThan(-1); expect(protocolGuardIndex).toBeLessThan(loginCallIndex); }); it("does not access browser password credentials on the login page", () => { const source = readLoginView(); expect(source).toContain('autocomplete="username"'); expect(source).toContain('name="username"'); expect(source).toContain('name="ctms-login-password"'); expect(source).toContain('autocomplete="new-password"'); expect(source).not.toContain('v-model="form.rememberPassword"'); expect(source).not.toContain("记住密码"); expect(source).not.toContain("tryLoadBrowserCredential"); expect(source).not.toContain("tryStoreBrowserCredential"); expect(source).not.toContain("navigator.credentials"); expect(source).not.toContain("PasswordCredential"); expect(source).not.toContain('localStorage.setItem("ctms_saved_password"'); expect(source).not.toContain("localStorage.setItem('ctms_saved_password'"); }); it("opens a CTMS-specific protocol dialog from the protocol text", () => { const source = readLoginView(); expect(source).toContain("authProtocolSections"); expect(source).toContain('v-model="protocolDialogVisible"'); expect(source).toContain('@click.stop.prevent="openProtocolDialog"'); expect(source).toContain('import { authProtocolSections } from "../content/authProtocol"'); expect(source).toContain("const protocolSections = authProtocolSections"); expect(source).toContain("confirmProtocol"); }); it("persists the protocol agreement checkbox in localStorage", () => { const source = readLoginView(); expect(source).toContain("const AGREE_PROTOCOL_KEY = \"ctms_agree_protocol\""); expect(source).toContain('localStorage.getItem(AGREE_PROTOCOL_KEY) === "true"'); expect(source).toContain('localStorage.setItem(AGREE_PROTOCOL_KEY, String(checked))'); expect(source).toContain("confirmProtocol"); }); it("avoids browser password caching", () => { const source = readLoginView(); expect(source).toContain('name="ctms-login-password"'); expect(source).toContain('autocomplete="new-password"'); expect(source).not.toContain('autocomplete="current-password"'); }); it("restores the current user's last project before routing after login", () => { const source = readLoginView(); const loginCallIndex = source.indexOf("auth.login(form.email, form.password)"); const userKeyIndex = source.indexOf("const userKey = auth.user?.email || form.email"); const restoreIndex = source.indexOf("studyStore.restoreStudyForUser(userKey"); const projectOverviewRouteIndex = source.indexOf('router.push("/project/overview")'); expect(userKeyIndex).toBeGreaterThan(loginCallIndex); expect(restoreIndex).toBeGreaterThan(loginCallIndex); expect(restoreIndex).toBeLessThan(projectOverviewRouteIndex); expect(source).toContain("preferActive: !!auth.user?.is_admin"); }); it("shows persistent logout reason notices on the login card", () => { const source = readLoginView(); expect(source).toContain('v-if="logoutNotice"'); expect(source).toContain("LOGOUT_REASON_TIMEOUT"); expect(source).toContain("LOGOUT_REASON_AUTH_EXPIRED"); expect(source).toContain("LOGOUT_REASON_MANUAL"); expect(source).toContain("30 分钟未检测到任何操作。为保护项目数据,请重新登录后继续。"); expect(source).toContain("登录状态已失效"); expect(source).toContain("已安全退出"); expect(source).toContain(".logout-notice"); }); it("keeps the login card wide enough on desktop while remaining responsive", () => { const source = readLoginView(); expect(source).toContain("width: clamp(480px, 34vw, 560px);"); expect(source).toContain("max-width: calc(100vw - 40px);"); }); it("falls back to full email input when no server-configured domains exist", () => { const source = readLoginView(); expect(source).toContain('fetchEmailDomains()'); expect(source).toContain("const availableEmailDomains = computed(() => configuredEmailDomains.value)"); expect(source).toContain("availableEmailDomains.value.length > 0 && availableEmailDomains.value.includes(form.emailDomain)"); expect(source).toContain("const manualEmailInput = computed(() => !hasConfiguredEmailDomains.value)"); expect(source).toContain('v-if="hasConfiguredEmailDomains"'); expect(source).toContain("未检测到邮箱后缀配置,请输入完整邮箱地址。"); expect(source).toContain("form.email = local;"); expect(source).toContain("if (domains.length === 0)"); expect(source).toContain("configuredEmailDomains.value.includes(domain)"); expect(source).not.toContain("preservedEmailDomain"); expect(source).not.toContain("showDomainSelect"); expect(source).not.toContain("email-domain-input"); }); });