Я провожу тестирование e2e для своего приложения с помощью Playwright (версия 1.44.1). У меня есть приложение React 18 (созданное с помощью Vite) для приложения блогов. Когда я запускаю приложение, пользовательский интерфейс показывает правильные реквизиты в компонентах React, а также в журналах консоли. Однако когда я запускаю тесты Playwright в графическом интерфейсе 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.
Если вы заметили в консоли Playwright GUI, реквизиты, полученные файлом Blog.jsx, форма объекта — это то, как он сохраняется в базе данных на бэкэнде:
{
title: first blog title,
author: first blog author,
url: first blog url,
likes: 0,
user: 665fac3a37d34c2da6c8f899
}
Теперь при запуске этого приложения в браузере Chrome реквизиты приобретают другую форму:
Просмотр в браузере Chrome. Обратите внимание, что в информации о блоге отображается имя пользователя, создавшего имя блога. Эта форма здесь правильная.
Форма реквизита в браузере Chrome: Во внешнем интерфейсе реквизиты, указанные ниже, отображаются правильно, а также правильно отражаются в пользовательском интерфейсе.
{
"title": "first blog title",
"author": "first blog author",
"url": "first blog url",
"likes": 0,
"user": {
"username": "first",
"name": "First Last",
"id": "665fac3a37d34c2da6c8f899"
},
"id": "665fac3b37d34c2da6c8f8a0"
}
См. реквизиты React Components в браузере Chrome:
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.info({ toDelete });
if (toDelete) {
blogService
.deleteBlog(blog)
.then(() => {
const blogList = blogs.filter((b) => blog.id !== b.id);
setBlogs(blogList);
console.info('Blog deleted');
})
.catch((err) => {
console.info(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.info('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.info('Blog.jsx', blog); // DEBUG
console.info('Blog creator username: ', blog.user.username); // DEBUG
console.info('Blog creator name: ', blog.user.name); // DEBUG
console.info('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;
да, вы правы, я исправил имя. Ошибка все еще возникает. Кроме того, в графическом интерфейсе драматурга обновлены имя и фамилия.
Пробовали ли вы зарегистрировать эти имена пользователей, чтобы узнать, каковы их значения и почему это сравнение не удалось, если вы ожидаете, что этого не произойдет? В React прямо перед рендерингом: console.info(blog.user.username, loggedInUsername, blog.user.name). Если несоответствие возникает не здесь, добавьте больше журналов во внешний и внутренний интерфейсы и посмотрите на сетевые запросы, пока не выясните, где именно несоответствие появляется в первую очередь. Вероятно, в этом и заключается проблема. Проблема в том, что mongoose не заполняет пользователя, когда вы используете PW, но делает это, когда вы делаете это вручную?
Я добавил эти журналы консоли и обновил изображение выше. Значения не определены в графическом интерфейсе тестирования Playwright, но они отображаются во внешнем интерфейсе. Что такое ПВ?
PW = Драматург. Если они не определены, вернитесь к несоответствию. Похоже, что "user": {...} где-то неправильно превращается в "user": "id string", так что я предполагаю, что у вас есть несоответствие в монго или что-то связано с локальным хранилищем, которое не настроено одинаково в двух средах. Лучше очистите локальное хранилище на обоих окружениях и выясните, где начинается несоответствие.
Кажется, я знаю проблему, но не знаю, как ее решить. Когда я добавляю новый блог, объектом является { title: first blog title, author: first blog author, url: first blog url, likes: 0, user: 665fac3a37d34c2da6c8f899 }. Обновление происходит не быстро. Я думаю, мне нужно исправить useEffect в App.jsx с помощью службы getAll, чтобы страница принимала новые значения при каждом новом добавлении. Я пытался добавить blogs в качестве зависимости к useEffect, но при этом мой бэкэнд работал бесконечно. Если я добавлю initialBlogs в качестве зависимости, это даст React Hook useEffect has an unnecessary dependency.





Я понял ответ. Я исправил свой POST-запрос в бэкэнд-контроллере blogs.js.
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);
});
После того, как я сохраню объект в исходной форме объекта. Я меняю форму при возврате значений в пользовательский интерфейс, добавляя поля имени пользователя и имени при заполнении объекта. Я сделал это так, потому что не хочу менять способ хранения значений в базе данных.
Я не знаю, является ли это эффективным способом достижения этой цели.
Теперь мои тесты на драматурга проходят без проблем. Пожалуйста, смотрите изображение ниже:
Разве
user.nameне должно бытьFirst Last, а неMatti Luukkainen? Если кнопкаremoveне отображается, тоblog.user.username === loggedInUsernameимеет ложное значение, поэтому кажется, что вы вошли в систему под другим пользователем (предположительно, Матти?), а не под именем пользователя, под которым вы выставили ложную публикацию в блоге.