我正在使用 Playwright(版本 1.44.1)为我的应用程序创建 e2e 测试。我有一个用于博客应用程序的 React 18 应用程序(使用 Vite 创建)。当我运行应用程序时,UI 在 React 组件以及控制台日志中显示正确的 props。但是,当我在 Playwright GUI 中运行 Playwright 测试时,道具显示不同的值,因此测试失败。我不知道这是怎么发生的。任何帮助,将不胜感激。以下是我的文件。
我有3个文件夹
问题:我不明白为什么正常运行时Chrome浏览器上的道具不同,而Playwright测试中的道具不同。道具形状应该是一样的。我在这里做错了什么?
我的测试文件
blog_app.spec.js
位于e2e
文件夹中:请注意,我已经编写了其他测试并且所有其他测试都通过了。
const { test, expect, beforeEach, describe } = require('@playwright/test');
const { loginWith, createBlog } = require('./helper');
describe('Blog app', () => {
beforeEach(async ({ page, request }) => {
// empty the database
await request.post('http:localhost:3003/api/testing/reset');
// create a user for the backend
await request.post('http://localhost:3003/api/users', {
data: {
name: 'First Last',
username: 'first',
password: 'last'
}
});
await page.goto('http://localhost:5173');
});
test('front page can be opened', async ({ page }) => { });
test('Login form is shown', async ({ page }) => { });
describe('Login', () => {
test('succeeds with correct credentials', async ({ page }) => { });
test('fails with wrong credentials', async ({ page }) => { });
});
describe('When logged in', () => {
beforeEach(async ({ page }) => {
await loginWith(page, 'first', 'last');
});
// THIS TEST DOES NOT WORK AS EXPECTED
test('a new blog can be created', async ({ page }) => {
await createBlog(page, 'first blog title', 'first blog author', 'first blog url');
await expect(page.locator('li').filter({ hasText: 'first blog title' })).toBeVisible();
await page.getByText('first blog title').locator('..').getByRole('button', { name: 'view' }).click();
await expect(page.getByText('Author: first blog author')).toBeVisible();
await expect(page.getByText('Likes: 0')).toBeVisible();
await expect(page.getByText('URL: first blog url')).toBeVisible();
await expect(page.getByText('Added by: ')).toBeVisible(); // THIS WORKS
await expect(page.getByText('Added by: First Last')).toBeVisible(); // THIS DOES NOT WORK
// await expect(page.getByRole('button', { name: 'remove' })).toBeVisible(); // THIS DOES NOT WORK
});
});
});
以下是 Playwright GUI 输出:
注意:名称、用户名和登录用户名在 Playwright GUI 中未定义。
如果您在 Playwright GUI 控制台中注意到,
Blog.jsx
文件收到的道具对象形状是如何保存到后端数据库的:
{
title: first blog title,
author: first blog author,
url: first blog url,
likes: 0,
user: 665fac3a37d34c2da6c8f899
}
现在在 Chrome 浏览器中运行此应用程序会收到不同的道具形状:
Chrome浏览器视图:请注意创建博客的用户名称出现在博客信息中。这个形状在这里是正确的。
Chrome 浏览器中的道具形状是:在前端,以下道具正确显示,并且也正确反映在 UI 中。
{
"title": "first blog title",
"author": "first blog author",
"url": "first blog url",
"likes": 0,
"user": {
"username": "first",
"name": "First Last",
"id": "665fac3a37d34c2da6c8f899"
},
"id": "665fac3b37d34c2da6c8f8a0"
}
在 Chrome 浏览器中查看 React Components 属性:
App.jsx
前端文件:
import { useEffect, useRef, useState } from 'react';
import Blog from './components/Blog';
import LoginForm from './components/LoginForm';
import NewBlogForm from './components/NewBlogForm';
import Notification from './components/Notification';
import Togglable from './components/Togglable';
import blogService from './services/blogs';
import loginService from './services/login';
const compareBlogs = (a, b) => {
if (a.likes < b.likes) return 1;
if (a.likes > b.likes) return -1;
return 0;
};
const sortBlogsAsPerMostLikedFirst = (blogs) => {
const sortedBlogs = blogs.sort(compareBlogs);
return sortedBlogs;
};
const App = () => {
const [blogs, setBlogs] = useState([]);
const blogFormRef = useRef();
const [notification, setNotification] = useState({
message: '',
isError: false,
});
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [user, setUser] = useState(null);
useEffect(() => {
blogService.getAll().then((initialBlogs) => {
const sortedBlogs = sortBlogsAsPerMostLikedFirst(initialBlogs);
setBlogs(sortedBlogs);
});
}, []);
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem('loggedBlogappUser');
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON);
setUser(user);
blogService.setToken(user.token);
}
}, []);
const removeNotificationAfterFiveSeconds = () => {
setTimeout(() => {
setNotification({
message: '',
isError: false,
});
}, 5000);
};
const handleLogin = async (event) => {
event.preventDefault();
try {
const user = await loginService.login({
username,
password,
});
window.localStorage.setItem('loggedBlogappUser', JSON.stringify(user));
blogService.setToken(user.token);
setUser(user);
} catch (error) {
setNotification({
message: error.response.data.error,
isError: true,
});
removeNotificationAfterFiveSeconds();
}
};
const handleLogout = async () => {
try {
window.localStorage.removeItem('loggedBlogappUser');
setUser(null);
setUsername('');
setPassword('');
} catch (exception) {
console.error(exception.message);
}
};
const addBlog = (blogObject) => {
blogFormRef.current.toggleVisibility();
blogService
.create(blogObject)
.then((returnedBlog) => {
setBlogs(blogs.concat(returnedBlog));
setNotification({
message: `SUCCESS: Added ${returnedBlog.title} blog`,
isError: false,
});
removeNotificationAfterFiveSeconds();
})
.catch((error) => {
setNotification({
message: error.response.data.error,
isError: true,
});
removeNotificationAfterFiveSeconds();
});
};
const increaseLikes = (blogToUpdate) => {
const blog = blogs.find((b) => b.id === blogToUpdate.id);
const updatedBlog = { ...blog, likes: blog.likes + 1 };
blogService
.update(blog.id, updatedBlog)
.then((returnedBlog) => {
const blogList = blogs.map((blog) => blog.id !== blogToUpdate.id ? blog : returnedBlog);
const sortedBlogs = sortBlogsAsPerMostLikedFirst(blogList);
setBlogs(sortedBlogs);
})
.catch((error) => {
setNotification({
message: `Blog '${blog.title}' was already removed from server`,
isError: true,
});
removeNotificationAfterFiveSeconds();
const blogList = blogs.filter((b) => b.id !== blogToUpdate.id);
setBlogs(blogList);
console.error(error.message);
});
};
const deleteBlog = (blog) => {
const toDelete = window.confirm(`Delete "${blog.title}" blog?`);
console.log({ toDelete });
if (toDelete) {
blogService
.deleteBlog(blog)
.then(() => {
const blogList = blogs.filter((b) => blog.id !== b.id);
setBlogs(blogList);
console.log('Blog deleted');
})
.catch((err) => {
console.log(err.message);
alert('Sorry! Blog cannot be deleted as it is not created by you.');
});
}
};
const loginForm = () => {
return (
<Togglable buttonLabel="login">
<LoginForm
username={username}
password={password}
handleUsernameChange={({ target }) => setUsername(target.value)}
handlePasswordChange={({ target }) => setPassword(target.value)}
handleSubmit={handleLogin}
/>
</Togglable>
);
};
const blogForm = () => (
<Togglable buttonLabel="new blog" ref={blogFormRef}>
<NewBlogForm createBlog={addBlog} />
</Togglable>
);
console.log('App.jsx', JSON.stringify(blogs)); // DEBUG
return (
<div>
<h1>Blogs</h1>
<Notification notification={notification} />
{!user && loginForm()}
{user && (
<div>
<p>
{user.name} logged in{' '}
<button type="button" onClick={handleLogout}>
logout
</button>
</p>
{blogForm()}
<ul style={{ listStyleType: 'none', padding: 0 }}>
{blogs.map((blog) => (
<Blog
key={blog.id}
blog={blog}
loggedInUsername={user.username}
handleLikes={() => increaseLikes(blog)}
handleDeleteBlog={() => deleteBlog(blog)}
/>
))}
</ul>
</div>
)}
</div>
);
};
export default App;
前端的
Blog.jsx
文件:
import { useState } from 'react';
const Blog = ({ blog, loggedInUsername, handleLikes, handleDeleteBlog }) => {
console.log('Blog.jsx', blog); // DEBUG
console.log('Blog creator username: ', blog.user.username); // DEBUG
console.log('Blog creator name: ', blog.user.name); // DEBUG
console.log('Logged in username: ', loggedInUsername, ); // DEBUG
// styles
const blogStyle = {
paddingLeft: 10,
paddingRight: 10,
paddingBottom: 10,
border: 'solid',
borderWidth: 1,
marginBottom: 5,
};
const removeBtnStyle = {
backgroundColor: 'lightblue',
padding: 5,
borderRadius: 10,
fontWeight: 'bold',
};
const [viewBlogInfo, setViewBlogInfo] = useState(false);
const toggleShow = () => {
setViewBlogInfo(!viewBlogInfo);
};
return (
<li style={blogStyle} className='blog'>
<p>
<span>{blog.title}{' '}</span>
<button onClick={toggleShow} className='viewHideBtn'>
{viewBlogInfo ? 'hide' : 'view'}
</button>
</p>
{viewBlogInfo && (
<div>
<div>
<div>Author: {blog.author}</div>
<div>
<span data-testid='likes'>Likes: {blog.likes}</span>
<button onClick={handleLikes} className='likeBtn'>like</button>
</div>
<div>URL: {blog.url}</div>
<div>Added by: {blog.user.name}</div>
</div>
<br />
{blog.user.username === loggedInUsername && (
<div>
<button style={removeBtnStyle} onClick={handleDeleteBlog} className='removeBtn'>
remove
</button>
</div>
)}
</div>
)}
</li>
);
};
export default Blog;
后端控制器的
blogs.js
文件:
const blogsRouter = require('express').Router();
const Blog = require('../models/blog');
const middleware = require('../utils/middleware');
blogsRouter.get('/', async (request, response) => {
const blogs = await Blog.find({}).populate('user', { username: 1, name: 1 });
response.json(blogs);
});
blogsRouter.get('/:id', async (request, response) => {
const blog = await Blog.findById(request.params.id).populate('user', { username: 1, name: 1 });
if (blog) {
response.json(blog);
} else {
response.status(404).end();
}
});
blogsRouter.post('/', middleware.userExtractor, async (request, response) => {
const user = request.user;
const body = request.body;
const blog = new Blog({
title: body.title,
author: body.author,
url: body.url,
likes: body.likes || 0,
user: user._id,
});
const savedBlog = await blog.save();
user.blogs = user.blogs.concat(savedBlog.id);
await user.save();
response.status(201).json(savedBlog);
});
blogsRouter.put('/:id', async (request, response, next) => { });
blogsRouter.delete('/:id', middleware.userExtractor, async (request, response) => { });
module.exports = blogsRouter;
我找到了答案。我在
blogs.js
后端控制器中修复了我的 POST 请求。
blogsRouter.post('/', middleware.userExtractor, async (request, response) => {
const user = request.user;
const body = request.body;
const blog = new Blog({
title: body.title,
author: body.author,
url: body.url,
likes: body.likes || 0,
user: user._id,
});
const savedBlog = await blog.save();
user.blogs = user.blogs.concat(savedBlog._id);
await user.save();
// convert the user id string field to user object with:
// user: {id: "", username: "", name: "W"}
const blogReturned = await savedBlog.populate('user', { username: 1, name: 1 });
response.status(201).json(blogReturned);
});
将对象保存为原始对象形状后。我通过在填充对象时添加用户名和名称字段来更改将值返回到 UI 时的形状。我这样做是因为我不想改变在数据库中存储值的方式。
我不知道这是否是实现这一目标的有效方法。
现在我的剧作家测试通过了,没有任何问题。请看下图: