)
一、需求背景在 Web 自动化测试平台中每个测试环境都对应不同的业务系统而多数系统需要登录后才能操作。如果每个测试用例都走一遍「输入账号密码 → 登录」流程会带来三个问题执行慢每个用例多花几秒到几十秒做登录不稳定验证码、登录频率限制、密码过期都会导致用例失败难维护登录方式变了要改所有用例因此我们做了一个「登录状态」功能把登录成功后的浏览器状态保存下来录制和执行测试时直接复用。二、整体设计核心思路是「环境 ↔ 登录状态一对多」环境(Environment) 1 ── 状态A(管理员) └── 状态B(普通用户) 环境(Environment) 2 ── 状态A每个登录状态对应一个storage_state.json文件文件命名规则pythondef generate_storage_state_filename(environment_id: int) - str: return fstate_env_{environment_id}_{uuid.uuid4().hex[:8]}.json文件统一存放在backend/storage_states/目录通过数据库记录EnvironmentStorageState表维护「环境-状态」的关联关系。backend/ └── storage_states/ ├── state_env_1_48b87a10.json ├── state_env_1_7073a307.json └── state_env_6_abfbf571.json三、登录状态的「录制」3.1 启动状态录制任务前端点击「更新登录状态」后后端创建一个录制任务pythonrouter.post(/start-state) async def start_state_recording(request: dict, ...): environment_id request.get(environment_id) state_id request.get(state_id) # 更新已有状态时传入 task_id str(uuid.uuid4())[:8] output_dir fC:\\records\\{task_id} # 确定保存路径更新时复用旧文件名新增时生成新文件名 if state_id: existing db.query(EnvironmentStorageState).filter( EnvironmentStorageState.id state_id).first() if existing: filename existing.filename if not filename: filename generate_storage_state_filename(environment_id) storage_state_path get_storage_state_path(environment_id, filename) # 写入任务参数 模式标记 params {target_url: target_url, storage_state_path: storage_state_path, ...} with open(os.path.join(output_dir, params.json), w) as f: json.dump(params, f, ensure_asciiFalse, indent2) with open(os.path.join(output_dir, MODE), w) as f: f.write(record_state)关键点任务目录里放一个MODE文件值为record_state。录制引擎启动时读取它就知道「这次是录登录状态不是录操作步骤」。3.2 录制引擎打开浏览器等待登录录制引擎基于 Playwright打开浏览器用户手动登录登录完成后关闭浏览器python# 等待用户关闭浏览器 await asyncio.wait_for(page.wait_for_event(close), timeout7200)四、登录状态的「保存」—— 这里有个大坑4.1 Playwright 原生能力storage_state()Playwright 提供了context.storage_state()方法可以导出 cookies 和 localStoragepythonstate_data await context.storage_state()但是storage_state()只保存 cookies 和 localStorage不保存 sessionStorage。很多老系统的登录 token 恰恰放在sessionStorage里例如我们的环境 6token 就是DGWL_SYSTEM_TOKEN存在 sessionStorage 中。这就导致录完登录状态 → 文件里只有 cookies 和 localStorage → 下次复用状态时sessionStorage 是空的 → 系统判定未登录 → 直接跳回登录页。我们最早录出来的状态文件只有 2KB、0 个 cookie就是一个典型的「没保存到 sessionStorage」的空状态。4.2 解决方案手动捕获 sessionStorage 并合并在浏览器关闭后除了storage_state()再用page.evaluate抓一次 sessionStorage合并写进同一个 JSONpythonif mode record_state: state_dest params.get(storage_state_path, ) # 1. Playwright 原生 statecookies localStorage try: state_data await context.storage_state() except: state_data {cookies: [], origins: []} # 2. 手动捕获 sessionStorage try: ss_data await page.evaluate(() JSON.stringify(window.sessionStorage)) ss json.loads(ss_data) if ss: state_data[sessionStorage] ss except: pass # 3. 写入文件 with open(state_dest, w, encodingutf-8) as f: json.dump(state_data, f, ensure_asciiFalse, indent2)最终文件结构json{ cookies: [...], origins: [ { origin: http://10.88.160.164:10001, localStorage: [ {name: DGWL_systemInfo, value: ...}, {name: DGWL_ENTERPRISEID, value: 2} ] } ], sessionStorage: { DGWL_SYSTEM_TOKEN: ab2ed98c-..., DGWL_USER_INFO: {...} } }五、登录状态的「复用」—— 注入回浏览器5.1 cookies localStoragePlaywright 原生支持创建浏览器上下文时直接指定storage_statepythoncontext_kwargs {} if state_file and os.path.exists(state_file) and os.path.getsize(state_file) 0: context_kwargs[storage_state] state_file context await browser.new_context(**context_kwargs)5.2 sessionStorageadd_init_script 注入sessionStorage 无法通过 storage_state 恢复需要用add_init_script在页面加载之前把数据写进去python# 读取 state 文件中的 sessionStorage with open(state_file, r, encodingutf-8) as f: state_data json.load(f) ss_data state_data.get(sessionStorage, {}) if ss_data: js_script ;.join( fsessionStorage.setItem({json.dumps(k)}, {json.dumps(v)}) for k, v in ss_data.items() ) await page.add_init_script(f() {{ {js_script}; }})add_init_script会在每次页面导航前执行保证 sessionStorage 数据在业务 JS 运行前就位登录态自然生效。5.3 复制到任务目录录制任务创建时把选中的状态文件复制到任务目录pythonif environment_id and storage_state_id: state db.query(EnvironmentStorageState).filter( EnvironmentStorageState.id storage_state_id, EnvironmentStorageState.environment_id environment_id ).first() source_path get_storage_state_path(environment_id, state.filename) if os.path.exists(source_path) and os.path.getsize(source_path) 0: dest_path os.path.join(output_dir, storage_state.json) shutil.copy2(source_path, dest_path)六、踩坑总结#坑现象解决1storage_state()不保存 sessionStorage复用的状态是假登录页面跳回登录页保存时手动page.evaluate抓 sessionStorage 合并写入2storage_state加载时不恢复 sessionStorage文件里有 sessionStorage 也没用add_init_script在页面加载前注入3状态文件来源域名不一致状态录自 A 站用在 B 站永远无效校验状态文件的 origin 与目标域名一致4录了个空状态文件只有几 KB、0 cookie确认登录成功后再关闭浏览器保存后检查文件大小与 sessionStorage 内容5更新状态时文件名变化数据库记录还指向旧文件更新时复用旧filename新增时才生成新文件名七、最终使用流程首次使用环境配置里「新增登录状态」→ 浏览器弹出 → 手动登录 → 关闭浏览器复用录制录制测试用例时选择该环境的登录状态 → 打开浏览器已是登录态 → 直接录操作批量执行每个用例绑定自己的状态文件 → 执行时自动加载状态过期环境里「更新登录状态」→ 重新登录覆盖旧状态