Ошибка теста Playwright: реквизиты возвращают разные значения в пользовательском интерфейсе тестирования Playwright и в реальном приложении React в браузере Chrome

Я провожу тестирование e2e для своего приложения с помощью Playwright (версия 1.44.1). У меня есть приложение React 18 (созданное с помощью Vite) для приложения блогов. Когда я запускаю приложение, пользовательский интерфейс показывает правильные реквизиты в компонентах React, а также в журналах консоли. Однако когда я запускаю тесты Playwright в графическом интерфейсе 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.

Если вы заметили в консоли 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;

Разве user.name не должно быть First Last, а не Matti Luukkainen? Если кнопка remove не отображается, то blog.user.username === loggedInUsername имеет ложное значение, поэтому кажется, что вы вошли в систему под другим пользователем (предположительно, Матти?), а не под именем пользователя, под которым вы выставили ложную публикацию в блоге.

ggorlen 05.06.2024 01:52

да, вы правы, я исправил имя. Ошибка все еще возникает. Кроме того, в графическом интерфейсе драматурга обновлены имя и фамилия.

qqq 05.06.2024 01:56

Пробовали ли вы зарегистрировать эти имена пользователей, чтобы узнать, каковы их значения и почему это сравнение не удалось, если вы ожидаете, что этого не произойдет? В React прямо перед рендерингом: console.info(blog.user.username, loggedInUsername, blog.user.name). Если несоответствие возникает не здесь, добавьте больше журналов во внешний и внутренний интерфейсы и посмотрите на сетевые запросы, пока не выясните, где именно несоответствие появляется в первую очередь. Вероятно, в этом и заключается проблема. Проблема в том, что mongoose не заполняет пользователя, когда вы используете PW, но делает это, когда вы делаете это вручную?

ggorlen 05.06.2024 01:59

Я добавил эти журналы консоли и обновил изображение выше. Значения не определены в графическом интерфейсе тестирования Playwright, но они отображаются во внешнем интерфейсе. Что такое ПВ?

qqq 05.06.2024 02:12

PW = Драматург. Если они не определены, вернитесь к несоответствию. Похоже, что "user": {...} где-то неправильно превращается в "user": "id string", так что я предполагаю, что у вас есть несоответствие в монго или что-то связано с локальным хранилищем, которое не настроено одинаково в двух средах. Лучше очистите локальное хранилище на обоих окружениях и выясните, где начинается несоответствие.

ggorlen 05.06.2024 02:16

Кажется, я знаю проблему, но не знаю, как ее решить. Когда я добавляю новый блог, объектом является { 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.

qqq 05.06.2024 02:41
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
0
6
53
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я понял ответ. Я исправил свой 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);
});

После того, как я сохраню объект в исходной форме объекта. Я меняю форму при возврате значений в пользовательский интерфейс, добавляя поля имени пользователя и имени при заполнении объекта. Я сделал это так, потому что не хочу менять способ хранения значений в базе данных.

Я не знаю, является ли это эффективным способом достижения этой цели.

Теперь мои тесты на драматурга проходят без проблем. Пожалуйста, смотрите изображение ниже:

Другие вопросы по теме