5.1 多窗口、iframe与ShadowDOM穿越


5.1 多窗口、iframe与ShadowDOM穿越

本节摘要:定位指令永远只作用于"当前上下文"——主文档、某个 iframe 的内部文档、某个窗口的页面、某层影子树的内部,都是彼此独立的上下文;找不到元素的第一嫌疑就是站错了地方。本节给出三套模板:窗口句柄的切换与回收、iframe 的进出与嵌套处理、影子树的逐层穿透,并配一张上下文地图。上一章的结构功夫在此兑现——这些模板应以页面对象或工具函数的形态沉淀,而不是散落在用例里。

一次点击,掉进另一个文档

工单 #027:用例点"在线客服"后一切操作失灵,报错"找不到元素",开发者工具里客服窗口的元素明明都在。排查一小时,真相是:点击打开了新标签页,脚本还站在旧标签页里对着旧文档喊话。工单 #031 更迷惑:登录页元素千真万确在页面上,脚本就是找不到——那个登录表单被包在第三方风控组件的 iframe 里。

两张工单共享同一个认知根源:浏览器里的"页面"不是一个大文档,而是文档套文档、窗口套窗口的嵌套结构。主文档只是入口,iframe 各自带独立文档,新标签页是完全独立的浏览上下文,影子树则是文档内的再封装。定位指令的作用域永远是"你当前站着的那个上下文"。所以复杂场景定位的第一步不是改选择器,而是回答:我的目标在哪个上下文里,我现在又在哪个上下文里。

图5-1 浏览器上下文嵌套地图

图5-1 浏览器上下文嵌套地图

窗口管理:切换、识别与回收

新标签页不会自动成为操作对象,脚本要用句柄显式切换。句柄是浏览器给每个窗口发的唯一编号,切换、枚举、回收都围绕它进行。下面是按标题或地址切换的实用模板:

def switch_to_window(driver, match_fn, timeout=10): """按条件找到目标窗口并切换;找不到抛超时""" end = time.time() + timeout while time.time() < end: for handle in driver.window_handles: driver.switch_to.window(handle) if match_fn(driver.current_url, driver.title): return handle time.sleep(0.2) raise TimeoutError("目标窗口未出现") new_handle = switch_to_window( driver, lambda url, title: "/payment" in url) # 按地址片段识别

三条纪律让它可靠。识别要用稳定特征(地址路径、页面标题),不要用句柄列表的位置——句柄顺序不保证新开的排最后,拿"最后一个"当"新窗口"是经典误用。切换后要等页面就绪再操作,新窗口的加载是异步的,切过去立刻定位等于重演 3.2 的工单一号。回收要成对:子流程结束调用 close() 关当前窗口,再切回主窗口句柄;quit() 是销毁整个会话,两者别混。忘切回就继续定位,是窗口类工单里出现频率最高的低级失误——模板函数把"切回"写进流程,用例想忘都忘不掉。

iframe:进出要成对,嵌套要逐层

iframe 的穿越规则比窗口还简单:进去、操作、出来。switch_to.frame() 接受三种参数——编号、name 或 id 属性、定位到的元素对象(推荐,语义最清晰);switch_to.default_content() 一步回到最外层主文档。要点在两个细节:

from contextlib import contextmanager @contextmanager def in_frame(driver, locator): """进入iframe操作,离开时自动回到主文档""" frame_el = WebDriverWait(driver, 10).until( EC.frame_to_be_available_and_switch_to_it(locator)) try: yield frame_el finally: driver.switch_to.default_content() with in_frame(driver, (By.CSS_SELECTOR, "[data-testid='risk-frame']")): driver.find_element(By.ID, "sms-code").send_keys("123456") # 出了with块,脚本已回到主文档

细节一,切换 iframe 也有时序:iframe 是独立文档,加载晚于主文档,刚渲染完就切会切进一个空文档。条件库里的"等 iframe 可用并切入"一步解决了等待与切换两件事。细节二,嵌套 iframe 必须逐层进:iframe 里再套 iframe,没有"一步到底"的切换,只能一层层切入;出来则可以一步跳回最外层。把上下文管理器嵌套使用,层级再深也不会迷路——finally 兜底保证任何异常路径都能回到主文档。

Shadow DOM:逐层穿透的封装墙

前端组件库为了样式与逻辑隔离,把内部结构藏进影子树——普通查询永远停在影子宿主的门外。穿透没有"一键直达",规则是:定位宿主,取影子根,在影子根内继续定位,一层层向内:

def expand_shadow_element(driver, element): return driver.execute_script("return arguments[0].shadowRoot", element) host = driver.find_element(By.CSS_SELECTOR, "payment-widget") root1 = expand_shadow_element(driver, host) inner_host = root1.find_element(By.CSS_SELECTOR, "card-form") root2 = expand_shadow_element(driver, inner_host) root2.find_element(By.CSS_SELECTOR, "input[name='cardno']").send_keys("4111")

穿之前先想一下要不要穿。影子树是组件的内部实现,组件库升级时内部结构说变就变——穿透代码天然脆。优先顺序永远是:组件对外暴露的普通属性、组件文档承诺的测试钩子、最后才是穿透。真要穿,把穿透封装成工具函数集中维护(如上例),组件库升级时改一处。

三套模板沉淀进工程

窗口模板、iframe 上下文管理器、影子穿透函数,加上 3.2 的工单经验,本节产出的是三个"贴纸上就能用"的模板。按第 4 章的规矩把它们放进工程的公共工具层:窗口与 iframe 模板挂在页面对象基类上,影子穿透进工具函数库。模板化的意义在第 6 章并行执行时再兑现一次——并行世界里,上下文管理不严谨的代码会以更高的频率炸出更诡异的故障。

收工清单

  • 上下文决定作用域:主文档、iframe 文档、窗口、影子树各是独立上下文;找不到先问"站在哪"。
  • 窗口三纪律:稳定特征识别、切换后等就绪、close 与 quit 分清、切回写成对动作。
  • iframe 成对进出:等可用再切入、嵌套逐层进、上下文管理器兜底回主文档。
  • 影子树先绕后穿:普通属性、测试钩子优先;穿透封装集中维护,跟紧组件库版本。
  • 模板化沉淀:本节产出进公共层,不是用例里的散装代码。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U