Playwright测试重复创建博客及删除功能测试失败求助
博客应用Playwright测试问题排查
问题现象
为MERN栈博客编写Playwright测试时,登录、博客创建测试正常通过,但测试博客删除功能时出现以下问题:
- 测试数据库中
Test blog 1和Test blog 2各被创建了两次,浏览器端也显示重复博客 - 删除测试本身未通过,移除该测试后,
A new blog can be created测试出现超时,报错信息显示等待.blog元素超时 - 手动或通过Postman测试后端增删功能均正常,仅Playwright测试出现异常
测试代码
主测试文件
const { test, expect, beforeEach, describe } = require("@playwright/test"); const { loginWith, createBlog } = require("./helper"); describe("Blog app", () => { beforeEach(async ({ page, request }) => { await request.post("/api/testing/reset"); await request.post("/api/users", { data: { name: "Matti Luukkainen", username: "mluukkai", password: "salainen", }, }); await page.goto("/"); }); test("Login form is shown", async ({ page }) => { await expect(page.getByText("log in to application")).toBeVisible(); await expect(page.getByTestId("username")).toBeVisible(); await expect(page.getByTestId("password")).toBeVisible(); await expect(page.getByRole("button")).toBeVisible(); }); describe("Login", () => { test("succeeds with correct credentials", async ({ page }) => { await loginWith(page, "mluukkai", "salainen"); await expect(page.getByText("Matti Luukkainen logged in")).toBeVisible(); }); test("fails with wrong credentials", async ({ page }) => { await loginWith(page, "mluukkai", "wrongpassword"); const notificationDiv = await page.locator(".notification"); await expect(notificationDiv).toContainText("wrong username or password"); await expect(notificationDiv).toHaveCSS("border-style", "solid"); await expect(notificationDiv).toHaveCSS("color", "rgb(255, 0, 0)"); await expect( page.getByText("Matti Luukkainen logged in") ).not.toBeVisible(); }); describe("When logged in", () => { beforeEach(async ({ page }) => { await loginWith(page, "mluukkai", "salainen"); }); test("A new blog can be created", async ({ page }) => { await createBlog(page, { title: "A blog created by playwright", author: "John Doe", url: "https://google.com/", }); const blog = await page.locator(".blog", { hasText: "A blog created by playwright", }); await expect(blog).toBeVisible(); }); describe("And a blog exists", () => { beforeEach(async ({ page }) => { await createBlog(page, { title: "Test blog 1", author: "John Doe", url: "https://yahoo.com", }); await createBlog(page, { title: "Test blog 2", author: "Jane Doe", url: "https://bing.com", }); }); test("A blog can be liked", async ({ page }) => { const blog = await page.locator(".blog", { hasText: "Test blog 1" }); await expect(blog).toBeVisible(); await blog.getByRole("button", { name: "view" }).click(); await blog.getByRole("button", { name: "like" }).click(); await expect(blog).toContainText("likes 1"); }); test("A blog can be deleted", async ({ page }) => { const blog = await page.locator(".blog", { hasText: "Test blog 2" }); await expect(blog).toBeVisible(); await blog.getByRole("button", { name: "view" }).click(); await blog.getByRole("button", { name: "remove" }).click(); page.once("dialog", (dialog) => { console.log(`Dialog message: ${dialog.message()}`); dialog.dismiss().catch(() => {}); }); await expect(blog).not.toBeVisible(); }); }); }); }); });
辅助函数文件
exports.loginWith = async (page, username, password) => { await page.getByTestId("username").fill(username); await page.getByTestId("password").fill(password); await page.getByRole("button", { name: "login" }).click(); }; exports.createBlog = async (page, { title, author, url }) => { try { await page.getByRole("button", { name: "add blog" }).click(); await page.getByTestId("title").fill(title); await page.getByTestId("author").fill(author); await page.getByTestId("url").fill(url); await page.getByRole("button", { name: "create" }).click(); await page.locator(".blog", { hasText: title }).waitFor(); } catch (error) { console.error("Error in createBlog:", error); throw error; } };
超时错误日志
@ blog_app.spec.js:62 Test timeout of 30000ms exceeded while running "beforeEach" hook. @ helper.js:15 Error: locator.waitFor: Test timeout of 30000ms exceeded. Call log: - waiting for locator('.blog').filter({ hasText: 'Test blog 1' }) to be visible
问题原因分析
- 对话框监听时机错误:删除测试中,
page.once("dialog")是在点击remove按钮之后注册的,此时Playwright可能已经触发了对话框事件,导致监听未捕获到对话框,页面因未处理的对话框卡住,测试超时。 - 对话框操作错误:使用
dialog.dismiss()取消删除操作,导致博客未被删除,测试断言expect(blog).not.toBeVisible()永远无法通过,进一步引发测试流程异常。 waitFor使用不当:辅助函数中用locator.waitFor()等待元素,相比Playwright推荐的expect断言,可靠性更低,容易因页面状态问题引发超时。
解决方案
1. 修正删除测试的对话框处理逻辑
将对话框监听移至点击remove按钮之前,并改为确认删除操作:
test("A blog can be deleted", async ({ page }) => { // 提前注册对话框监听,确保捕获事件 page.once("dialog", async (dialog) => { console.log(`Dialog message: ${dialog.message()}`); await dialog.accept(); // 确认删除操作 }); const blog = page.locator(".blog", { hasText: "Test blog 2" }); await expect(blog).toBeVisible(); await blog.getByRole("button", { name: "view" }).click(); await blog.getByRole("button", { name: "remove" }).click(); await expect(blog).not.toBeVisible(); });
2. 优化辅助函数的等待逻辑
将locator.waitFor()替换为expect断言,符合Playwright最佳实践:
exports.createBlog = async (page, { title, author, url }) => { try { await page.getByRole("button", { name: "add blog" }).click(); await page.getByTestId("title").fill(title); await page.getByTestId("author").fill(author); await page.getByTestId("url").fill(url); await page.getByRole("button", { name: "create" }).click(); // 用expect断言等待博客元素出现,更可靠 await expect(page.locator(".blog", { hasText: title })).toBeVisible(); } catch (error) { console.error("Error in createBlog:", error); throw error; } };
验证修改效果
- 删除测试可正常通过,博客被正确删除,数据库中不再出现重复创建的测试博客
- 移除删除测试后,其他测试也不会再出现超时问题
- 所有测试均能稳定通过
内容的提问来源于stack exchange,提问作者Stephen
相关产品推荐
相关产品推荐

