本节摘要:文件上传、下载与浏览器原生弹窗共享一个特征——它们不属于页面 DOM,页面级的定位与点击技巧全部失效,必须改走"浏览器外壳"的通道:上传往输入元素塞路径、下载配启动参数加完成判定、弹窗切到 alert 上下文用专用接口应答。本节给出一套"分清归属、各走各道"的处理清单,把这一类最难缠也最常被回避的场景收编进工程。
用例写到文件与弹窗就卡住,几乎是每个自动化工程师的必经之路。卡住的原因不是代码难写,而是用错了武器:对着系统的文件选择窗口点坐标(那是操作系统控件,Selenium 够不着)、等下载完成的判定靠固定延时(下载多久结束全看网路)、对着原生弹窗找元素(它压根不在文档里)。本节先把"归属"这个判断立起来:遇到失灵先问目标归谁管——归页面,用定位与点击;归浏览器外壳,用本节的专用通道;归操作系统,设法绕开,让用例根本不触碰它。
网页上传的底层是一个文件输入元素,浏览器用它唤起系统文件选择窗口。自动化不走那条路——Selenium 可以直接把本地文件路径"塞"给输入元素,等效于用户选好了文件点确定:
import pathlib file_input = wait.until(EC.presence_of_element_located( (By.CSS_SELECTOR, "input[type='file']"))) file_input.send_keys(str(pathlib.Path("fixtures/invoice.pdf").resolve())) submit.click() toast = wait.until(EC.visibility_of_element_located( (By.CSS_SELECTOR, "[data-testid='upload-toast']"))) assert "成功" in toast.text
三个细节决定成败。路径必须是绝对路径,相对路径取决于进程工作目录,本地过、流水线挂的又一经典案例——resolve() 一步到位。组件库的"美化上传"要先找到真输入元素:很多上传按钮是自绘的,真正的输入元素藏在 DOM 深处甚至隐藏着,用批量定位找出所有文件输入元素,对隐藏的输入元素塞路径依然有效(存在即可,不要求可见)。自定义拖拽上传区没有输入元素,走动作链模拟拖放往往不可靠,务实做法是与前端约定测试钩子,或用脚本后门直接派发放置事件——这是 3.4 说的后门逃生通道的正当用途。
下载自动化分三步:告诉浏览器把文件下到哪、触发下载动作、等文件真正落盘。第一步靠启动参数与偏好设置:
import time download_dir = str(pathlib.Path("downloads").resolve()) options = webdriver.ChromeOptions() options.add_experimental_option("prefs", { "download.default_directory": download_dir, # 指定下载目录 "download.prompt_for_download": False, # 不弹保存对话框 "savefile.default_directory": download_dir, }) driver = webdriver.Chrome(options=options)
第二步触发下载(正常点击即可)。第三步是最容易草率的:轮询下载目录,等目标文件出现且"下载中"的临时后缀消失——浏览器下载未完成时文件名带临时扩展名,只判断"文件存在"会拿到半个文件:
def wait_for_download(directory, filename, timeout=30): end = time.time() + timeout while time.time() < end: target = pathlib.Path(directory, filename) partial = pathlib.Path(directory, filename + ".crdownload") if target.exists() and not partial.exists(): return target time.sleep(0.3) raise TimeoutError(f"下载未完成:{filename}")
流式导出(点击导出后后台生成文件)同理,只是等待目标从"文件落盘"换成"页面提示就绪加文件落盘"双重条件。把等待函数收进工程工具层,与 3.3 的等待体系并列——它们是同一思想在不同领域的应用:轮询条件,不赌时间。
浏览器原生弹窗(警告、确认、输入)由浏览器外壳绘制,不在任何文档里。Selenium 的通道是切到弹窗上下文:
alert = WebDriverWait(driver, 5).until(EC.alert_is_present()) print("弹窗文本:", alert.text) alert.send_keys("我的备注") # 仅输入型弹窗有效 alert.accept() # 点确定;dismiss() 等效点取消
三类弹窗三个要点。警告型只有"确定",确认型有确定与取消——用例要两条路径都覆盖,取消路径往往才是业务缺陷的高发区;输入型先 send_keys 再确认,注意弹窗上下文里的输入没有定位与等待,接口比页面原始得多。最容易翻车的是"意外弹窗":系统崩溃上报、会话过期确认这类不速之客会在任意时刻挡住页面,让所有定位集体失灵。工程级解法是把"有弹窗就处理"做成每个页面动作前的全局守卫(在第 4 章夹具层挂载),或者至少在排错时养成反射:定位集体失灵,先看有没有弹窗挡路。
至于打印预览、系统权限授予这类真·操作系统对话框,结论明确:自动化通道不存在,用例设计时就该绕开——需要授予权限的用例用浏览器启动参数预授权,需要打印的场景验证导出文件而不是模拟打印。让用例不触碰操作系统层,比攻克它划算得多。
问:上传之后怎么断言"文件内容被正确处理"? 分两层。页面层断言用户可见结果:上传成功的提示、缩略图出现、列表多出一行记录。数据层如果需要验证文件本身(比如服务端解析结果),通过接口查询处理结果再断言,而不是继续在页面上绕。原则与 1.2 一脉相承:页面验证呈现,接口验证数据,各测各的。
把三种场景串成一个完整演练:写一条"资料上传"用例——塞路径给输入元素完成上传,处理上传确认的原生确认弹窗(接受),最后配置下载目录验证导出的处理报告落盘。这条演练覆盖本节全部三条通道,跑通之后再读一遍代码,你会注意到三种通道的公共心智:先判断归属,再选通道,最后等条件确认结果。
浏览器外壳的角落清完,还剩最后一块特殊地形:屏幕只有巴掌大的移动世界。下一节评估 Selenium 在那里的能力边界与替代方案。