Files
Yb-QaAutomationHub/tests/web/admin/save_auth.py
T
xst ac95f3004f fix(save_auth): 登录检测改为探测页实测会话有效性
不再读取用户标签页的 URL 判定——SPA hash 跳转后脚本读到的
可能是旧值, 登录界面也可能在其它标签页, 两种情况都会把成功
登录误判为失败。

改为: 回车后在同一 context 新开探测页访问首页, 由应用自己
回答是否已登录(有会话落 #/home, 无会话被重定向回 #/login),
与测试用例 ensure_login 的判定方式一致; 探测完即关, 不动
用户标签页。路由判断用精确 hash/path 解析, 参数含 login
字样不再误报。

验证: 用用户误报场景下保存的登录态实测, 访问直达 #/home,
证实会话判定有效。
2026-09-04 15:33:03 +08:00

177 lines
6.5 KiB
Python
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.
#!/usr/bin/env python3
"""手动登录并保存浏览器登录态(cookies + localStorage)。
目的: 绕过图形验证码——人工完成一次验证码登录后,
cookies + localStorage 序列化到 JSON 文件,后续用例自动复用。
用法:
python tests/web/admin/save_auth.py
流程:
1. 脚本打开浏览器到登录页
2. 自动填入账号、密码(从 tests/config/test_config.yaml 读取)
3. 你在浏览器中手动完成图形验证码并点击登录
4. 登录成功后按 Enter 键,脚本自动保存登录态
5. 后续 pytest 用例自动复用
生成的 auth 文件:
tests/web/auth/super_admin_chromium.json
"""
import argparse
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
# Windows 中文控制台为 GBK 编码,emoji 等字符无法编码会导致 print 崩溃;
# 这里把所有无法编码的字符替换为 ?,保证脚本在任何终端都能运行。
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(errors="replace")
from tests.core.config_loader import load_config
from tests.core.playwright_support import fast_shutdown
# 登录页元素选择器(Elementor 全路径)。密码框带 is-error 类,仅在错误态出现时匹配,
# 因此 _fill_credentials 会先尝试这些选择器,失败再退回占位符/类型选择器。
USERNAME_SELECTOR = (
"#app > div > form > div:nth-child(2) > div "
"> div.elInput.el-input.el-input--medium > input"
)
PASSWORD_SELECTOR = (
"#app > div > form > div.el-form-item.el-tooltip.is-error.is-required.el-form-item--medium "
"> div > div.elInput.el-input.el-input--medium > input"
)
def _fill_credentials(page, account: dict) -> bool:
"""自动填写账号密码,用户名/密码独立处理,任一失败不影响另一个。"""
filled_user = False
for user_sel in (USERNAME_SELECTOR, "input[placeholder*='账号']"):
try:
page.wait_for_selector(user_sel, timeout=8000)
page.fill(user_sel, account["phone"])
filled_user = True
break
except Exception:
continue
filled_pwd = False
for pwd_sel in (PASSWORD_SELECTOR, "input[type='password']"):
try:
page.wait_for_selector(pwd_sel, timeout=5000)
page.fill(pwd_sel, account["password"])
filled_pwd = True
break
except Exception:
continue
return filled_user and filled_pwd
def _is_on_login_page(url: str) -> bool:
"""精确判断是否仍在登录页:只看路由本身(hash 或 path)。
不做子串匹配——登录成功后的 URL 可能带 login 字样的参数
(如 ?from=login、#/loginLog 菜单),子串判断会把成功登录误判为失败。
"""
from urllib.parse import urlparse
parsed = urlparse(url)
frag = (parsed.fragment or "").lstrip("/") # #/login → "login"
path = (parsed.path or "").rstrip("/") # /admin/login → "/admin/login"
frag_is_login = frag == "login" or frag.startswith("login/") or frag.startswith("login?")
path_is_login = path == "/login" or path.endswith("/login")
return frag_is_login or path_is_login
def main():
parser = argparse.ArgumentParser(
description="手动登录并保存浏览器登录态(绕过图形验证码)"
)
parser.add_argument(
"--role",
default="super_admin",
help="登录角色(默认: super_admin),会体现在文件名中",
)
parser.add_argument(
"--browser",
default="chromium",
choices=["chromium", "firefox", "webkit"],
help="浏览器类型(默认: chromium)",
)
args = parser.parse_args()
config = load_config()
web_config = config["admin"]
base_url = web_config["base_url"]
login_url = f"{base_url}/#/login"
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser_launcher = getattr(p, args.browser)
browser = browser_launcher.launch(headless=False)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
locale="zh-CN",
)
page = context.new_page()
print(f"\n打开登录页: {login_url}")
page.goto(login_url)
account = config.get("accounts", {}).get(args.role)
if account and _fill_credentials(page, account):
print(f"[{args.role}] 账号密码已自动填写")
else:
print(f"[{args.role}] 自动填写失败/无配置,请在浏览器中手动输入账号密码")
print(f"\n{'='*60}")
print(f" 请手动完成【图形验证码】并点击【登录】")
print(f" 登录成功后回到此处按 Enter 键保存登录态...")
print(f"{'='*60}\n")
input(">>> 按 Enter 保存登录态...")
# 不依赖用户标签页的 URL(SPA hash 跳转后读到的可能是旧值, 登录
# 界面也可能在别的标签页)——在 context 里新开探测页访问首页,
# 由应用自己回答"是否已登录": 有会话落在 #/home, 无会话被重定向
# 回 #/login。探测完成即关闭, 不动用户的标签页。
# (与测试用例 ensure_login 的判定方式一致)
print("[检测] 验证登录状态...")
probe = context.new_page()
try:
probe.goto(base_url, wait_until="domcontentloaded")
probe.wait_for_timeout(3000) # SPA hash 路由重定向的 settle
current = probe.url
finally:
probe.close()
if _is_on_login_page(current):
print(f"\n[警告] 登录态无效,访问首页被重定向回登录页: {current}")
print(" 登录可能未成功(验证码错误/未点击登录)。仍要保存吗?(y/n): ", end="")
choice = input().strip().lower()
if choice != "y":
print("已取消。")
fast_shutdown(p)
sys.exit(0)
print(f"\n当前页面: {current}")
auth_dir = Path(web_config.get("storage_state_dir", "tests/web/auth"))
auth_dir.mkdir(parents=True, exist_ok=True)
auth_file = auth_dir / f"{args.role}_{args.browser}.json"
context.storage_state(path=str(auth_file))
print(f"登录态已保存到: {auth_file}")
# 强杀浏览器与驱动,跳过 Playwright 在本机 ~30s 的优雅关闭等待
fast_shutdown(p)
print(f"\n现在可以跑了: pytest tests/web/admin/ -m p0")
if __name__ == "__main__":
main()