Playwright 测试失败:道具在 Playwright 测试 UI 和 Chrome 浏览器上的实际 React 应用程序中返回不同的值

问题描述 投票:0回答:1

我正在使用 Playwright(版本 1.44.1)为我的应用程序创建 e2e 测试。我有一个用于博客应用程序的 React 18 应用程序(使用 Vite 创建)。当我运行应用程序时,UI 在 React 组件以及控制台日志中显示正确的 props。但是,当我在 Playwright GUI 中运行 Playwright 测试时,道具显示不同的值,因此测试失败。我不知道这是怎么发生的。任何帮助,将不胜感激。以下是我的文件。

我有3个文件夹

  • e2e(与剧作家)
  • 博客后端
  • 博客前端

问题:我不明白为什么正常运行时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 test props

注意:名称、用户名和登录用户名在 Playwright GUI 中未定义。

如果您在 Playwright GUI 控制台中注意到,

Blog.jsx
文件收到的道具对象形状是如何保存到后端数据库的:

{
  title: first blog title, 
  author: first blog author, 
  url: first blog url, 
  likes: 0, 
  user: 665fac3a37d34c2da6c8f899
}

现在在 Chrome 浏览器中运行此应用程序会收到不同的道具形状:

Chrome浏览器视图:请注意创建博客的用户名称出现在博客信息中。这个形状在这里是正确的。

Blogs app Chrome browser

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 属性:

React components props

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;
reactjs react-props e2e-testing playwright-test
1个回答
0
投票

我找到了答案。我在

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 时的形状。我这样做是因为我不想改变在数据库中存储值的方式。

我不知道这是否是实现这一目标的有效方法。

现在我的剧作家测试通过了,没有任何问题。请看下图:

Playwright Passing Tests

© www.soinside.com 2019 - 2024. All rights reserved.