从测试搜索功能的角度,如何优化下面的代码?
test("The explicit waits", async ({ page }) => {
await page.goto("https://blog.martioli.com/playwright-tips-and-tricks-2/")
await page.getByText('Playwright tips and tricks #2').scrollIntoViewIfNeeded()
await expect(page.getByText('Playwright tips and tricks #2')).toBeVisible()
await expect(page.getByRole('button', { name: 'Search this site' })).toBeVisible()
await page.getByRole('button', { name: 'Search this site' }).click()
await expect(page.frameLocator('iframe[title="portal-popup"]').getByPlaceholder('Search posts, tags and authors')).toBeVisible()
await page.frameLocator('iframe[title="portal-popup"]').getByPlaceholder('Search posts, tags and authors').fill("Cypress")
await expect(page.frameLocator('iframe[title="portal-popup"]').getByRole('heading', { name: 'Cypress' }).first()).toContainText("Cypress")
});
优化建议:
• 移除所有的toBeVisible()
期望。
• 删除scrollIntoViewIfNeeded()
。
• 将iframe
存储在常量中,以便重用和提高可读性。
• 使用正则表达式来匹配部分文本,例如使用/Search posts/
代替Search posts
, tags and authors
下面这段代码会发生什么?
test("The visible methods", async ({ page }) => {
await page.goto("https://blog.martioli.com/");
await expect(page.getByRole('link', { name: 'About' }).isVisible())
});
可能的答案:
• 测试失败,因为isVisible()
不是一个有效的方法。
• 测试失败,错误信息为Property 'then' not found
。
• 测试通过。
答案:
• 测试失败,因为isVisible()
不是一个有效的方法。 原因是isVisible()
返回的是一个Promise
对象,因此需要使用await
来解决异步问题。
给定下面的代码,预期会发生什么?
test("The ninja", async ({ page }) => {
await page.goto("https://www.clickspeedtester.com/mouse-test/");
await page.getByRole('link', { name: 'Second Clicker' }).click({ trial: true })
await page.waitForURL("**/clicks-per-second-test/")
})
可能的答案:
• 测试失败,错误信息为page.waitForURL
: Test ended, because click was not performed
。
• 测试失败,因为waitForURL()
的参数格式不正确。
• 测试失败,因为click
步骤中没有trial: true
这个选项。
答案: 测试会在click
步骤失败,因为click
方法不支持trial: true
选项。
以下代码会发生什么,如何改进?
test("The you OK", async ({ page }) => {
const response = await page.request.get('https://blog.martioli.com/');
await expect(response).toBeOK();
})
可能的答案:
• 测试失败,因为toBeOK()
不是一个有效的方法。
• 测试失败,因为page
对象没有request
属性。
• 测试通过。
答案: 测试会失败,因为toBeOK()
方法不存在。应改为expect(response.status()).toBe(200)
。
innerText()
的使用假设元素的文本内容是“Be the first to discover new tips and tricks about automation in software development”
,下面的代码会发生什么?
test("The innerText?", async ({ page }) => {
await page.goto('https://blog.martioli.com');
const innertText = page.locator(".gh-subscribe-description").innerText()
await expect(innertText).toContain("Be the first to discover new tips")
});
可能的答案:
• 测试通过。
• 测试失败,错误信息为Error: expect Received object: {}
。
• 测试失败,因为innerText()
不能使用toContain()
。
答案: 测试会失败,错误信息为Error: expect Received object: {}
。 innerText()
方法返回一个Promise
对象,应该使用await
来获取实际的文本内容,或者使用textContent()
方法。
如何最有效地过滤测试用例?
答案: 最有效的方式是使用标签或注解来分类测试用例,结合Playwright
的过滤机制,使用test.describe
和test.only
来控制哪些测试需要执行。例如:
test.describe('smoke tests', () => {
test('basic test', async () => { ... });
});
test.only('smoke test for login', async () => { ... });
给定下面的代码,假设我不是宇航员,你认为会发生什么?
test("The fail", async ({ page }) => {
test.fail();
await page.goto("https://www.martioli.com/");
await expect(page.getByText('Astronaut')).toBeVisible()
});
可能的答案:
• 测试通过,因为test.fail()
被调用。
• 测试失败,因为test.fail()
会强制测试失败。
• 测试会执行所有步骤,但结果仍会标记为失败。
答案: 测试会通过,因为test.fail()
表示预期测试会失败。由于没有找到"宇航员"这一文本,预期失败的结果反而导致了测试的“通过”。
考虑下面的代码,可能会发生什么,并如何改进?
const locales = [
"de",
"com",
"es"
]
for (const location of locales) {
test(`check health: ${location}`, async ({ page }) => {
const response = await page.request.get(`https://www.google.${location}/`)
expect(response).toBeOK()
});
}
可能的答案:
• 测试通过。
• 测试失败,因为无法执行这样的for循环。
• 测试失败,因为expect没有await关键字。
答案: 测试会失败,因为expect(response).toBeOK()
未正确等待响应。应该使用await
来等待异步结果:
await expect(response).toBeOK();
getByText()
的错误使用给定下面的代码,预期会发生什么?
test("The page one", async ({ page }) => {
await page.goto("https://blog.martioli.com/");
await expect(getByText('Recommended Resources')).toBeVisible()
});
可能的答案:
• 测试通过,因为页面中确实有"Recommended Resources"
。
• 测试失败,因为出现ReferenceError
。
• 测试失败,因为页面没有"Recommended Resources"
这一文本。
答案: 测试会失败,错误信息为ReferenceError: getByRole is not defined。
正确的写法应该是page.getByText()
,而不是直接使用getByText
。
这些问题涵盖了Playwright
中的显式等待、测试过滤、请求处理以及错误处理等多个方面。掌握这些问题的答案,可以帮助你在面试中展示出对Playwright
的深刻理解。祝大家面试顺利并且取得理想的工作!
使用Playwright
时学到的技巧。这些技巧从实际项目中,遇到的各种问题,在文档和教程中通常不容易找到,希望对大家掌握这些技巧有所帮助。
会使用page.locator()
来提及Playwright
的定位器方法,但这并不意味着我建议只使用locator()
方法。它只是一个占位符。对于大多数情况,建议使用Playwright
内置的定位器方法,尤其是getByTestId()
,如果这些方法不起作用,再考虑使用.locator()
。
ID
的情况下找到子元素假设你有一个结构如下的HTML
:
text you want
如果多个span
标签有不同的文本,你可以使用父元素或祖父元素进行定位,Playwright
会遍历所有子元素并提取所有文本。此时,expect(uniqueID).toHaveText("text you want")
会成功。但是,如果你只想定位包含特定文本的子元素,可以使用page.getByTestId(uniqueIDParent).filter({ hasText: "text you want" })
,这样可以通过过滤器精准定位。
这种错误通常是由于你在代码中漏掉了await
。Playwright
是异步的,意味着所有操作都是基于Promise
的。为了保证步骤按顺序执行并避免竞争条件,必须使用await
来确保每个步骤的顺序执行。尽管VS Code
有时会提示某些await
是多余的,但实际上,你确实需要它们。
Playwright
的自动等待机制以下是Selenium
中你不再需要显式等待的几种情况:
• 不再需要等待元素可见再与之交互。Playwright
会自动检查元素是否可见、是否附加到DOM
中、以及是否稳定(动画完成后)。
• 当你打开页面或点击链接跳转时,不再需要显式检查页面是否加载完成,Playwright
会自动等待页面加载完成后再进行交互。
• 不再需要显式等待元素出现或消失。Playwright
内置了超时机制,会自动等待并尝试查找元素,默认超时时间为5
秒。
有时你可能会发现,Playwright
的默认超时设置无法满足需求。如果没有其他办法,可以使用waitForTimeout()
来强制等待一定时间。虽然这种做法一般不推荐,但在某些特殊情况下,它可能是唯一的选择。
Playwright
中断言字符串数组?你可以使用expect(locator).toHaveText(array)
来断言一个数组中的每个元素都存在。Playwright
会在后台逐一检查数组中的每个项。
Playwright
的定位器方法可以同时查找一个或多个元素。如果你想处理多个元素,可以使用page.locator(multipleElements)
来查找它们,但返回的并不是一个简单的元素数组,而是一个单独的定位器对象。如果你需要操作所有的元素,可以在方法后加上.all()
来获取所有匹配的元素。例如:page.locator(multipleElements).all()
。
当你有多个具有文本的元素时,可以使用page.locator(parentOfElementsWithText).allTextContents()
或.allInnerTexts()
来提取它们的文本内容。需要注意的是,这种方法可能会包含换行符、逗号或额外的空格,因此不建议用于精确文本匹配,但它在使用expect(locator).toContain()
时非常有用。
Playwright
中断言元素不存在的技巧:expect(locator).toHaveCount(0)
。这种方法类似于Selenium
中的findElements
,如果找不到元素,它会返回一个空数组,不会导致测试失败。更推荐的做法是使用Playwright
内置的not
操作符:locator(element).not.toBeVisible()
。一般来说,所有的断言方法都可以与not结合使用。
getByTestId()
不够用时该怎么办?在某些情况下,比如需要组合父子元素来定位特定数据时,getByTestId()
可能无法满足需求。这时,可以考虑使用and操作符来组合多个定位器。例如:page.getByText(elem).and(page.getByText(elem))
。另一种选择是使用过滤器定位器,它也可以与not
操作符一起使用,进一步优化查询。
你可以将一个父元素存储在常量中,然后在后续的代码中使用它来执行操作。例如:
const element = page.locator("parentElement");
element.click();
element.getByTestId("childElement");
这种做法的好处是,每次调用element
时,它会重新查询DOM
,确保你操作的是最新的元素。
$(locator)
或$$(multiple)
在Playwright
中,使用和来获取元素句柄是不推荐的做法。这是因为在某些情况下,使用会返回一个已经过时的DOM
元素,可能导致类似Selenium
中的StaleElementReferenceException
错误。
有时,应用的响应时间较长,比如点击提交按钮后会出现加载动画。在这种情况下,你可以为特定操作增加超时,例如:
expect(locator).toBeVisible({ timeout: 20000 });
通过在方法中传递timeout
选项,可以覆盖默认的超时设置,避免超时错误。
expect
有时不支持toHaveText()
方法?当你给expect传递一个标准的定位器时,它会支持所有Web
元素的断言方法,但如果你传递的是一个修改过的对象(例如使用了innerText()
方法),它就会变成一个类似Jest
的对象,因此无法使用Playwright
的方法。Playwright
的expect
会根据传入对象的类型来自动选择正确的断言方法。
ID
属性?Playwright
默认使用data-testid
作为测试ID
,但如果你的Web应用使用了不同格式的ID
,你可以在配置中设置testIdAttribute
来指定不同的测试ID
。例如:
projects: [
{
name: "new-app",
use: {
testIdAttribute: "id",
baseURL: "https://newapp.domain.com",
},
},
{
name: "legacy-app",
use: {
testIdAttribute: "data-testid",
baseURL: "https://legacyapp.domain.com",
},
},
]
这样,你可以在不同的项目中使用不同的ID
格式。
这些技巧涵盖了Playwright
中的一些常见问题和最佳实践,掌握这些技巧可以帮助你提高自动化测试的效率,减少调试时间,同时避免一些常见的错误。希望这些分享对你有所帮助,祝你在使用Playwright
时更加得心应手!
会使用page.locator()
来提及Playwright
的定位器方法,但这并不意味着我建议只使用locator()
方法。它只是一个占位符。对于大多数情况,建议使用Playwright
内置的定位器方法,尤其是getByTestId()
,如果这些方法不起作用,再考虑使用.locator()
。
在某些情况下,元素可能在页面完全加载后才出现在DOM中。此时,我们可以使用以下方法:
• 等待网络空闲:page.goto('https://playwright.dev/', {waitUntil: 'networkidle'})
• 等待元素的特定状态:使用 element.waitFor("attached")
等待元素出现在DOM
中。
使用 expect()
时,你可以自定义断言失败时的错误信息,例如:
await expect(locatorOrValue, 'Failed to perform something').toBe();
这对于自定义方法或页面方法中调试特别有用。
如果需要验证注册表单提交后是否收到确认邮件,可以使用Playwright的 expect.poll()
方法进行轮询:
await expect.poll(async () => {
const allEmails = await page.request.get('https://api.email.com/allEmails');
// 查找包含注册信息的邮件
return emailCode;
}, {
message: 'Failed to find confirmation link in email',
intervals: [1000, 2000, 10000],
timeout: 60000
}).toBeTruthy()
这样可以确保即使邮件迟迟未到,也能在超时之前持续检查。
Playwright
的 expect.toPass()
方法允许我们执行多个断言,直到它们全部通过。例如,如果你需要断言一个元素的状态变化,可以像这样:
await expect(async () => {
await expect(page.getByText("LOADING")).toBeVisible();
await expect(page.getByText("COMPLETE.")).toBeVisible();
}).toPass({
intervals: [1000, 5000, 10000],
timeout: 60000
});
在Playwright中
,可以通过 waitForResponse()
拦截特定的网络请求,并验证其返回的数据。这对于前端与后端的联合调试尤为重要:
const invoiceCall = page.waitForResponse("**/invoices/*");
await page.getByText("Generate Invoice").click();
const response = await invoiceCall;
const responseAsJson = await response.json();
await expect(responseAsJson.invoice.value).toBe("355");
Playwright
提供了调试工具 npx playwright test --debug
,可以帮助你查看执行的每一步,并详细了解背后发生的操作。
有时你需要从元素中提取文本并在后续步骤中使用,可以通过 page.locator(locator).innerText()
获取并存储文本,注意,innerText()
只会获取可见文本。如果需要获取隐藏文本,可以使用 textContent()
。
在执行测试时,您可能想要查看一些特定的测试数据,比如当前环境、配置或测试数据等。Playwright
提供了 testInfo
对象,让你能够实时访问这些信息。以下是如何访问和使用它的示例:
import { test } from "@playwright/test";
test.describe('Test Suite Name', () => {
test('Test Name', async ({ page }, testInfo) => {
console.log(`Test Name: ${testInfo.title}`);
console.log(`Parallel Index: ${testInfo.parallelIndex}`);
console.log(`Shard Index: ${JSON.stringify(testInfo.config.shard)}`);
});
});
通过 testInfo
对象,你可以获取与测试相关的各种信息,比如测试名称、并发索引、分片索引等,这对于复杂项目的调试和数据分析非常有帮助。
Playwright
允许你在一个测试中启动多个浏览器窗口,每个窗口都有独立的存储和 cookies
。这在测试像聊天功能这样的应用时尤为重要。下面是如何在一个测试中模拟两个用户聊天的代码示例:
import { test, expect } from "@playwright/test";
test("Two users chat functionality", async ({ browser }) => {
// 启动两个浏览器上下文,每个用户都有独立的存储和 cookies
const user1Context = await browser.newContext();
const user1Page = await user1Context.newPage();
const user2Context = await browser.newContext();
const user2Page = await user2Context.newPage();
// 打开聊天页面
await user1Page.goto("https://www.yourweb.com/chat");
await user2Page.goto("https://www.yourweb.com/chat");
// 用户1发送消息
await user1Page.locator("#input").type("Hello user 2");
await user1Page.locator("#sendMsgBtn").click();
// 验证用户2是否收到消息
await expect(user2Page.locator("text=Hello user 2")).toBeVisible();
// 用户2发送消息
await user2Page.locator("#input").type("Oh ! Hello user 1");
await user2Page.locator("#sendMsgBtn").click();
// 验证用户1是否收到消息
await expect(user1Page.locator("text=Oh ! Hello user 1")).toBeVisible();
});
在 Playwright
中,处理多个标签页非常简单。假设你点击一个链接,目标是打开一个新标签页,只需使用 context.waitForEvent('page')
监听页面事件。以下是如何在 Playwright 中处理多个标签页的示例:
import { test } from "@playwright/test";
test("Handle multiple tabs in the same browser", async ({ page }) => {
// 点击链接打开新标签页
const pagePromise = page.context().waitForEvent('page');
await page.locator("text=Open New Tab").click();
// 获取新标签页
const newPage = await pagePromise;
await newPage.goto("https://example.com");
// 在新标签页中进行操作
await expect(newPage.locator("text=Example")).toBeVisible();
});
你可能会想同时在多个浏览器(如 Chromium
、Firefox
、Webkit
)上运行测试。Playwright
允许你直接在测试中启动不同类型的浏览器,并进行并行测试。以下是如何在一个测试中使用多个浏览器的示例:
import { test, webkit, firefox, chromium } from "@playwright/test";
test("Multiple browser drivers", async () => {
const browser1 = await webkit.launch();
const context1 = await browser1.newContext();
const page1 = await context1.newPage();
await page1.goto("https://martioli.com/");
const browser2 = await firefox.launch();
const context2 = await browser2.newContext();
const page2 = await context2.newPage();
await page2.goto("https://martioli.com/");
});
Playwright
配置你可以在测试中临时覆盖配置,而不必修改全局配置文件。这对于不同测试用例需要不同配置的情况非常有用。例如,可以覆盖浏览器的视口大小或地理位置:
import { test } from "@playwright/test";
test.use({
geolocation: { longitude: 36.095388, latitude: 28.0855558 },
userAgent: 'my super secret Agent value'
});
test("Override config", async ({ page }) => {
await page.goto("https://martioli.com/");
});
此外,你还可以通过在不同的测试套件中使用 test.use()
来为每个套件设置不同的配置。
Playwright
中使用 Promise.all
在 Playwright
中,许多事件(如 waitForResponse()
、waitForRequest()
、waitForEvent()
)需要与其触发器并行执行。为了避免事件竞争条件,可以使用 Promise.all()
同时执行多个异步操作。以下是一个使用 Promise.all()
的示例:
const [response] = await Promise.all([
page.locator("button").click(),
page.waitForResponse("https://example.com/api/search")
]);
这样,click()
和 waitForResponse()
方法会并行执行,避免了等待响应的延迟,提升了测试效率。
总结
这些技巧将帮助你更高效地使用Playwright
进行自动化测试,减少常见的调试难题并提升测试的稳定性