常见问题,自动化效率的技巧

1. 显式等待优化

从测试搜索功能的角度,如何优化下面的代码?

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 poststags and authors

2. 关于isVisible()的理解

下面这段代码会发生什么?

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来解决异步问题。

3. “忍者点击”问题

给定下面的代码,预期会发生什么?

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.waitForURLTest ended, because click was not performed

  • • 测试失败,因为waitForURL()的参数格式不正确。

  • • 测试失败,因为click步骤中没有trial: true这个选项。

答案: 测试会在click步骤失败,因为click方法不支持trial: true选项。

4. 请求的健康检查

以下代码会发生什么,如何改进?

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)

5. 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()方法。

6. 最佳的测试过滤方式

如何最有效地过滤测试用例?

答案: 最有效的方式是使用标签或注解来分类测试用例,结合Playwright的过滤机制,使用test.describetest.only来控制哪些测试需要执行。例如:

test.describe('smoke tests', () => {
  test('basic test', async () => { ... });
});
test.only('smoke test for login', async () => { ... });

7. 故意使测试失败

给定下面的代码,假设我不是宇航员,你认为会发生什么?

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()表示预期测试会失败。由于没有找到"宇航员"这一文本,预期失败的结果反而导致了测试的“通过”。

8. 健康检查代码的改进

考虑下面的代码,可能会发生什么,并如何改进?

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();

9. 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()

10. 如何在只有父元素有唯一ID的情况下找到子元素

假设你有一个结构如下的HTML

 
   
      text you want    
 

如果多个span标签有不同的文本,你可以使用父元素或祖父元素进行定位,Playwright会遍历所有子元素并提取所有文本。此时,expect(uniqueID).toHaveText("text you want")会成功。但是,如果你只想定位包含特定文本的子元素,可以使用page.getByTestId(uniqueIDParent).filter({ hasText: "text you want" }),这样可以通过过滤器精准定位。

11. 为什么有时会收到“浏览器已关闭”的错误?

这种错误通常是由于你在代码中漏掉了awaitPlaywright是异步的,意味着所有操作都是基于Promise的。为了保证步骤按顺序执行并避免竞争条件,必须使用await来确保每个步骤的顺序执行。尽管VS Code有时会提示某些await是多余的,但实际上,你确实需要它们。

12. Playwright的自动等待机制

以下是Selenium中你不再需要显式等待的几种情况:

  • • 不再需要等待元素可见再与之交互。Playwright会自动检查元素是否可见、是否附加到DOM中、以及是否稳定(动画完成后)。

  • • 当你打开页面或点击链接跳转时,不再需要显式检查页面是否加载完成,Playwright会自动等待页面加载完成后再进行交互。

  • • 不再需要显式等待元素出现或消失。Playwright内置了超时机制,会自动等待并尝试查找元素,默认超时时间为5秒。

13. 时间超时不起作用时的解决方案

有时你可能会发现,Playwright的默认超时设置无法满足需求。如果没有其他办法,可以使用waitForTimeout()来强制等待一定时间。虽然这种做法一般不推荐,但在某些特殊情况下,它可能是唯一的选择。

14. 如何在Playwright中断言字符串数组?

你可以使用expect(locator).toHaveText(array)来断言一个数组中的每个元素都存在。Playwright会在后台逐一检查数组中的每个项。

15. 如何处理多个元素?

Playwright的定位器方法可以同时查找一个或多个元素。如果你想处理多个元素,可以使用page.locator(multipleElements)来查找它们,但返回的并不是一个简单的元素数组,而是一个单独的定位器对象。如果你需要操作所有的元素,可以在方法后加上.all()来获取所有匹配的元素。例如:page.locator(multipleElements).all()

16. 如何处理大段文本?

当你有多个具有文本的元素时,可以使用page.locator(parentOfElementsWithText).allTextContents().allInnerTexts()来提取它们的文本内容。需要注意的是,这种方法可能会包含换行符、逗号或额外的空格,因此不建议用于精确文本匹配,但它在使用expect(locator).toContain()时非常有用。

17. 如何断言元素不存在?

Playwright中断言元素不存在的技巧:expect(locator).toHaveCount(0)。这种方法类似于Selenium中的findElements,如果找不到元素,它会返回一个空数组,不会导致测试失败。更推荐的做法是使用Playwright内置的not操作符:locator(element).not.toBeVisible()。一般来说,所有的断言方法都可以与not结合使用。

18. 当getByTestId()不够用时该怎么办?

在某些情况下,比如需要组合父子元素来定位特定数据时,getByTestId()可能无法满足需求。这时,可以考虑使用and操作符来组合多个定位器。例如:page.getByText(elem).and(page.getByText(elem))。另一种选择是使用过滤器定位器,它也可以与not操作符一起使用,进一步优化查询。

19. 如何使用一个父元素进行多次操作?

你可以将一个父元素存储在常量中,然后在后续的代码中使用它来执行操作。例如:

const element = page.locator("parentElement");
element.click();
element.getByTestId("childElement");

这种做法的好处是,每次调用element时,它会重新查询DOM,确保你操作的是最新的元素。

20. 不要使用$(locator)$$(multiple)

Playwright中,使用和来获取元素句柄是不推荐的做法。这是因为在某些情况下,使用会返回一个已经过时的DOM元素,可能导致类似Selenium中的StaleElementReferenceException错误。

21. 如果应用响应时间超过5秒怎么办?

有时,应用的响应时间较长,比如点击提交按钮后会出现加载动画。在这种情况下,你可以为特定操作增加超时,例如:

expect(locator).toBeVisible({ timeout: 20000 });

通过在方法中传递timeout选项,可以覆盖默认的超时设置,避免超时错误。

22. 为什么expect有时不支持toHaveText()方法?

当你给expect传递一个标准的定位器时,它会支持所有Web元素的断言方法,但如果你传递的是一个修改过的对象(例如使用了innerText()方法),它就会变成一个类似Jest的对象,因此无法使用Playwright的方法。Playwrightexpect会根据传入对象的类型来自动选择正确的断言方法。

23. 如何处理多个数据测试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()

 

24 如何处理页面加载完成后才出现的元素

在某些情况下,元素可能在页面完全加载后才出现在DOM中。此时,我们可以使用以下方法:

  • • 等待网络空闲:page.goto('https://playwright.dev/', {waitUntil: 'networkidle'})

  • • 等待元素的特定状态:使用 element.waitFor("attached") 等待元素出现在DOM中。

25. 自定义断言失败信息

使用 expect() 时,你可以自定义断言失败时的错误信息,例如:

await expect(locatorOrValue, 'Failed to perform something').toBe();

这对于自定义方法或页面方法中调试特别有用。

26. 如何验证是否收到邮件

如果需要验证注册表单提交后是否收到确认邮件,可以使用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()

这样可以确保即使邮件迟迟未到,也能在超时之前持续检查。

27. 如何在断言失败时不让测试失败

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
});

28. 如何拦截网络请求

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");

29. 如何像专业人士一样调试

Playwright提供了调试工具 npx playwright test --debug,可以帮助你查看执行的每一步,并详细了解背后发生的操作。

30. 如何获取元素的文本并存储以备后用

有时你需要从元素中提取文本并在后续步骤中使用,可以通过 page.locator(locator).innerText() 获取并存储文本,注意,innerText() 只会获取可见文本。如果需要获取隐藏文本,可以使用 textContent()

 

31. 实时查看测试详情

在执行测试时,您可能想要查看一些特定的测试数据,比如当前环境、配置或测试数据等。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 对象,你可以获取与测试相关的各种信息,比如测试名称、并发索引、分片索引等,这对于复杂项目的调试和数据分析非常有帮助。

32. 测试多个浏览器窗口

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();
});

33. 处理多个标签页

在 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();
});

34. 在一个测试中使用多个浏览器

你可能会想同时在多个浏览器(如 ChromiumFirefoxWebkit)上运行测试。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/");
});

35. 在测试中覆盖 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() 来为每个套件设置不同的配置。

36. 如何在 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进行自动化测试,减少常见的调试难题并提升测试的稳定性

 

你可能感兴趣的:(playwright,自动化,数据库,java,单元测试,测试工具)