点击蓝字
关注我们
上期我们详细探究了POM的落地方案、页面操作与断言分工,这次将继续学习封装交互操作、处理路由网络请求、浏览器事件等。
在页面类里封装交互操作
POM 有一条硬性规范:所有页面交互动作都必须封装成页面类内部函数。这样我们就能在操作前后处理各类特殊场景,不用每条用例重复写前置、后置逻辑。
单纯点击按钮看起来简单,但实际开发中总会有各种额外场景需要兼容。
拿全站通用的导航栏举例,导航栏包含多项操作能力,我们需要实现三类功能:
●获取页面标题
●查看全部语言版本并完成页面跳转
●唤起搜索弹窗
拿语言下拉菜单来讲,这个下拉框必须鼠标悬浮才会展开。如果不做 POM 封装,任何需要操作下拉菜单的用例,都要重复写悬浮、等待面板渲染、点击选项整套流程。
但用上 POM 之后,我们可以封装函数自动完成悬浮、等待下拉框展示、执行点击操作。
// header_page.ts async allLanguages(): Promise<string[]> {await this.LANGUAGE_DROPDOWN.hover();return await this.LANGUAGE_DROPDOWN_LINK_LIST.allInnerTexts();}async getCurrentLanguage(): Promise<string> {await this.LANGUAGE_DROPDOWN.hover();await expect.soft(this.LANGUAGE_DROPDOWN_CURRENT_ITEM).toBeVisible();return await this.LANGUAGE_DROPDOWN_CURRENT_ITEM.innerText();}async clickLanguage(env: Languages): Promise<void> {await this.LANGUAGE_DROPDOWN.hover();await expect.soft(this.LANGUAGE_DROPDOWN_CURRENT_ITEM).toBeVisible();await this.LANGUAGE_DROPDOWN_LINK_LIST.getByText(env).click();}
补充说明:上面 header_page.ts 代码中用到了软断言。软断言只会校验页面状态,就算校验失败也不会直接终止整条用例运行。它能大幅减少硬编码waitForTimeout等待,同时严格遵循规范:所有硬性断言全部写在测试脚本内部。
封装完成后,测试脚本可读性极高,逻辑一目了然:
// home_tests.spec.ts test("Supported Languages", { tag: ["@e2e", "@home"] }, async ({ page }) => {const header = home_page.HEADER;expect(await header.getCurrentLanguage()).toBe(Languages.NODEJS);expect(home_page.GET_STARTED_BUTTON).toHaveAttribute("href", "/docs/intro");await header.clickLanguage(Languages.PYTHON);await expect(home_page.HEADER.HEADER).toHaveText("Playwright for Python");expect(await header.getCurrentLanguage()).toBe(Languages.PYTHON);expect(home_page.GET_STARTED_BUTTON).toHaveAttribute("href","/python/docs/intro");});
这里划重点:页面类里绝对不要封装可通过 Playwright 自动重试断言实现的逻辑!不要再写isButtonVisible()这类工具函数。元素可见性校验直接写在测试用例中,调用类顶层定义的定位器变量即可。
下面是打开、关闭搜索弹窗的测试用例示例:
// home_tests.spec.ts test("Open/Close Search",{ tag: ["@e2e", "@home", "@search"] },async ({ page }) => {const search_dialog = await home_page.HEADER.openSearchPage();await expect(search_dialog.DIALOG).toBeVisible();await search_dialog.escSearch();await expect(search_dialog.DIALOG).not.toBeVisible();});
在页面类中统一处理路由网络请求
接下来我们讲解搜索模块:输入关键词后会发起网络请求拉取搜索结果。
拦截、捕获网络请求是 Playwright 最亮眼的功能之一,借助它我们能跳出单纯 UI 校验,实现更深层次的业务状态校验,精准把控页面运行情况。
但使用路由拦截有一个硬性前提:必须先注册路由监听函数,再触发页面操作。行业通用标准写法是用Promise.all([])同时包裹监听注册逻辑和触发操作逻辑。
放到 POM 架构里会出现一个问题:触发页面操作的逻辑全部封装在页面类内部方法中,我们不能把重复的路由拦截代码复制粘贴到每一个点击、输入方法内。
解决方案是借助TypeScript的lambda匿名函数,TypeScript函数支持把其他函数作为入参。
我们封装一套通用工具函数,接收「操作触发器」作为参数,内部用Promise.all统一处理监听注册和页面操作。同一段路由拦截逻辑可以在所有页面类中复用,完全不会破坏 POM 封装原则。
路由请求执行完成后,我们可以把 Response 响应对象返回给测试脚本做断言,也能把响应存为页面类的成员变量。
重点提醒:必须从 Playwright 导入 Response 类型,如果使用 IDE 默认推荐的 Node 原生 Response,会直接报类型错误。
// network_request_handler.ts import { Page, Response } from "@playwright/test";export type Fulfill = {status?: number;headers?: {};body?: string;};exportconst captureQueries = async (page: Page,trigger: Promise<void>): Promise<Response> => {const [response] = await Promise.all([page.waitForResponse((response) =>response.url().includes("/queries") && response.request().method() === "POST"),trigger,]);return response;};exportconst fulfillQueries = async (page: Page,trigger: Promise<void>,fulfillment: Fulfill): Promise<void> => {const [response] = await Promise.all([page.route("**/queries", (route) => route.fulfill(fulfillment)),trigger,]);};
在搜索弹窗页面类中调用这套网络工具:
// search_dialog_page.ts async enterSearch(text: string): Promise<Response> {return await captureQueries(this.page, this.SEARCH_INPUT.fill(text));}async enterSearchAndMockFail(text: string): Promise<void> {await fulfillQueries(this.page, this.SEARCH_INPUT.fill(text), {status: 400,});}
对应的搜索功能测试用例:
// search_tests.spec.ts test("Search Results", { tag: ["@e2e", "@search"] }, async ({ page }) => {const search_dialog = await home_page.HEADER.openSearchPage();await expect(search_dialog.DIALOG).toBeVisible();const response = await search_dialog.enterSearch("test");expect(response.status()).toBe(200);await expect(search_dialog.RESULTS_LIST).toBeVisible();});test("Search Results Invalid",{ tag: ["@e2e", "@search"] },async ({ page }) => {const search_dialog = await home_page.HEADER.openSearchPage();await expect(search_dialog.DIALOG).toBeVisible();const response = await search_dialog.enterSearchAndMockFail("test");await expect(search_dialog.RESULTS_LIST).toBeHidden();});
在页面类统一处理浏览器事件
浏览器事件的处理逻辑和上面的路由拦截高度相似,存在同一个限制、同一套解决思路:必须先注册事件监听,再触发页面操作。
和网络响应不同,浏览器弹窗事件不会直接返回规整结构化对象。我们可以自定义 Type 类型承载弹窗信息,返回给测试脚本做断言,用法和 Response 响应对象完全一致。
可惜 Playwright 官网找不到适配演示的弹窗事件场景,下面给一套通用可落地的事件处理工具模板:
// event_handler.ts import { Page } from "@playwright/test";export type Popup = {message: String;};exportconst getPopupAndDismiss = async (page: Page,trigger: Promise<void>): Promise<Popup> => {let message = "";await Promise.all([page.once("dialog", async (dialog) => {message = dialog.message();await dialog.dismiss();}),page.waitForEvent("dialog"),trigger,]);return { message: message };};exportconst getPopupAndAccept = async (page: Page,trigger: Promise<void>): Promise<Popup> => {let message = "";await Promise.all([page.once("dialog", async (dialog) => {message = dialog.message();await dialog.accept();}),page.waitForEvent("dialog"),trigger,]);return { message: message };};
收尾:POM 长久稳定的核心价值
POM 专门解决大规模自动化套件落地后的维护难题。当你的用例数量增长到几十、上百条时,POM 分层架构的优势会彻底凸显出来:
●定位器、页面交互操作全部收拢在统一页面类,前端产品页面改版时,只用修改一处代码就能完成全量更新
●所有断言逻辑严格留在测试脚本内,业务流程分层清晰,每一条用例的测试目的一目了然
●借助 lambda 匿名函数封装通用工具,接口拦截、浏览器事件监听这类深度测试能力可以轻松复用
Playwright 是当下体验极佳的现代化自动化工具,而 POM 是适配所有自动化工具、经过行业长期验证的成熟设计模式。
把过往沉淀的规范经验,结合 Playwright 这类新式工具落地,我们就能搭建一套长期稳定、便于迭代维护的自动化测试框架。
聊聊你的看法
有想法、疑问都可以留言交流。如果你不知道从哪切入思考,可以参考下面几个讨论方向:
可探讨问题
●你第一次接触自动化测试代码时,遇到过哪些棘手麻烦?
●POM 能不能解决你当前自动化项目里现存的痛点?
●POM 能不能降低新人测试工程师的上手门槛,也方便开发同学读懂自动化代码?
●POM 该如何结合测试驱动开发(TDD)流程使用?
落地执行建议
●和团队同步沟通,梳理现有自动化脚本的维护痛点
●复盘存量自动化用例,找出大量重复代码,统一抽成页面类封装方法
●如果脚本频繁随机失败,排查是否存在大量硬编码等待逻辑,这类场景用 POM 重构后稳定性会大幅提升
●POM 架构处理网络拦截十分便捷,可以考虑补充接口层相关自动化校验
E n d


