基于 ReactJS 创建购物车应用和测试用例执行
让我们看一个使用 JSX 作为前端的购物车应用程序。它是 React 广泛使用的一种类似 XML/HTML 的语法,它扩展了 ECMAScript,因此类似 XML/HTML 的文本也可以与 JavaScript/React 代码一起应用。
创建 React App 并安装模块:
第 1 步:使用创建反应应用程序
npx create-react-app
Eg: npx create-react-app shoppingcart
第 2 步:移动到文件夹
cd shoppingcart
第 3 步:安装所需的依赖项
npm install @babel/core
npm i babel-runtime#Here i stands for install
npm i @testing-library/jest-dom
npm i @testing-library/react
npm i @testing-library/user-event
npm i autoprefixer
npm i enzyme
npm i enzyme-adapter-react-16
npm i react
npm i react-dom
npm i react-scripts
或者,我们可以指定 package.json 中的所有内容,而不是一一进行,如下所示,从命令提示符中我们可以给出:
npm install
它将负责安装依赖项中提到的所有包。可以从 package.json 验证使用的包:
包.json
{
"name": "shoppingcart",
"version": "1.0.0",
"description": "shoppingcart",
"main": "app/main.jsx",
"scripts": {
"lint": "eslint 'app/**/*.@(js|jsx)'",
"test": "react-scripts test",
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject"
},
"dependencies": {
"babel-runtime": "~6.2.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"autoprefixer": "^9.8.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-scripts": "3.4.3"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"keywords": [
"react",
"test",
"enzyme"
],
"pre-commit": [
"lint"
],
"devDependencies": {
"babel-eslint": "~4.1.6",
"chai": "^3.4.1",
"html-webpack-plugin": "^5.3.2",
"react-addons-test-utils": "^15.4.1",
"webpack": "^5.55.1",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.3.0",
"jsdom": "^7.2.2"
}
}
项目文件夹结构:项目应如下所示:
示例:让我们开始项目:
App.js
import React from 'react';
import './App.css';
// That means it is referring the jsx file
// present under src/app/components folder
import App1 from './app/components/App';
function App() {
return (
);
}
export default App;
App.css
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #006400;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.main-wrapper {
display: flex;
justify-content: center;
}
table {
margin: 1rem;
}
table th td,
table tr td {
border: 1px solid black;
border-collapse: collapse;
}
form {
margin: 1rem;
}
input {
line-height: 1.4rem;
margin-left: 1rem;
}
h5 {
display: block;
}
form button {
line-height: 1.4rem;
background-color: #ffffff;
cursor: pointer;
}
.cities-wrapper {
border: 1px solid black;
margin: 1rem;
padding: 1rem;
align-items: flex-end;
height: fit-content;
}
ul {
float: center;
margin-top: 0;
}
.center {
margin-left: auto;
margin-right: auto;
}
.shelf-wrapper {
text-align: left;
display: flex;
justify-content: center;
}
.shelf-wrapper h4 {
text-align: center;
}
.shelf-wrapper .shelf {
width: 19%;
border: 1px solid black;
display: inline-block;
min-height: 15rem;
}
.shelf span {
/* width: 60%; */
}
.shelf button {
float: right;
/* width: 40%; */
}
.book-wrapper span {
width: 100%;
}
.shelf table tr td {
border: none;
}
ItemJSON.js
import { EventEmitter } from 'events';
import assign from 'object-assign';
// Initially specifying the constant items just as an sample
const ProductStore = assign({}, EventEmitter.prototype, {
items: {
products: [
{ productId: 0, productName: 'Samsung',
productPrice: 10000, productQuantity: 2 },
{ productId: 1, productName: 'Motorola',
productPrice: 7000, productQuantity: 3 },
{ productId: 2, productName: 'Redmi',
productPrice: 8000, productQuantity: 4 },
]
},
nextproductId: 3,
// To get all the items and display in the screen
getAll: function getAll() {
return this.items;
},
emitChange: function emitChange() {
this.emit('change');
},
// When an item is added
addChangeListener: function addChangeListener(callback) {
this.on('change', callback);
},
// When an item is removed
removeChangeListener: function removeChangeListener(callback) {
this.removeListener('change', callback);
},
addNewProducts: function addNewProducts(product) {
const products = this.items.products;
if (!products ||
typeof this.items.products.length !== 'number') {
this.items.products = [];
}
product.productId = this.nextproductId++;
product.done = false;
this.items.products.push(product);
},
deleteProducts: function deleteProducts(productId) {
this.items.products = this.items.products.filter(
(product) => product.productId !== productId);
}
});
export default ProductStore;
App.jsx
import React from 'react';
import AddItems from './AddItems';
import List from './List';
export default class App extends React.Component {
render() {
return (
Available Products
// List.jsx is enclosed
);
}
}
List.jsx
import React from 'react';
import ItemJSON from '../Items/ItemJSON';
import ListItems from './ListItems';
export default class ProductList extends React.Component {
constructor(props) {
super(props);
this.state = ItemJSON.getAll();
}
componentDidMount() {
ItemJSON.addChangeListener(this._onChange.bind(this));
}
componentWillUnmount() {
ItemJSON.removeChangeListener(this._onChange.bind(this));
}
_onChange() {
this.setState(ItemJSON.getAll());
}
render() {
const ListItemsList = this.state.products.map(
product => {
return (
);
});
return (
// All the items present in
// ItemJSON.js is displayed here
{ListItemsList}
);
}
}
Javascript
import React from 'react';
import ItemJSON from '../Items/ItemJSON';
export default class AddItems extends React.Component {
//will pick the added product and added
addItems() {
const newProductName = this.refs.product.value;
const newPrice = this.refs.price.value;
const newQuantity = this.refs.quantity.value;
if (newProductName) {
ItemJSON.addNewProducts({
productName: newProductName,
productPrice: newPrice,
productQuantity: newQuantity
});
ItemJSON.emitChange();
this.refs.product.value = '';
this.refs.price.value = '';
this.refs.quantity.value = '';
}
}
render() {
return (
Product Name
Price
Quantity
Action
);
}
}
ListItems.jsx
import React from 'react';
import ItemJSON from '../Items/ItemJSON';
export default class ListItems extends React.Component {
// This code is meant for deletion
deleteProduct(e) {
e.preventDefault();
ItemJSON.deleteProducts(this.props.product.productId);
ItemJSON.emitChange();
}
render() {
const product = this.props.product;
return (
// displaying available products and it
// is having delete action
{product.productName}
{product.productPrice}
{product.productQuantity}
);
}
}
App.test.js
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import { expect } from 'chai';
import { shallow, mount, configure } from 'enzyme';
import TestUtils from 'react-dom/test-utils';
import App from './app/components/App';
import jsdom from 'jsdom';
import { findDOMNode } from 'react-dom';
configure({ adapter: new Adapter() });
beforeAll(() => {
global.fetch = jest.fn();
// window.fetch = jest.fn(); if running browser environment
});
let wrapper;
beforeEach(() => {
wrapper = shallow(< App />, { disableLifecycleMethods: true });
});
afterEach(() => {
wrapper.unmount();
});
if (typeof document === 'undefined') {
global.document = jsdom.jsdom(
'');
global.window = document.defaultView;
global.navigator = global.window.navigator;
}
describe('DOM Rendering', function () {
it('Add functionality to add new products
by clicking add', function () {
const app = TestUtils.renderIntoDocument( );
const appDOM = findDOMNode(app);
let productItemsLength =
appDOM.querySelectorAll('.todo-text').length;
let addInput = appDOM.querySelector('input');
addInput.value = 'Add item';
let addButton = appDOM.querySelector('.add-todo button');
TestUtils.Simulate.click(addButton);
console.log(appDOM.querySelectorAll('.todo-text').length);
expect(appDOM.querySelectorAll('.todo-text')
.length).to.be.equal(productItemsLength + 3);
// As after adding we will get additional value 3.
});
});
describe('DOM Rendering', function () {
it('On deleteing, the item should get deleted', function () {
const app = TestUtils.renderIntoDocument( );
let productItems = TestUtils
.scryRenderedDOMComponentsWithTag(app, 'li');
let productLength = productItems.length;
let deleteButton = productItems[0]
.querySelector('button');
TestUtils.Simulate.click(deleteButton);
let productItemsAfterClick = TestUtils
.scryRenderedDOMComponentsWithTag(app, 'li');
expect(productItemsAfterClick.length)
.to.equal(productLength - 1);
});
});
describe('Enzyme Shallow', function () {
it('App\'s title should be Available Products', function () {
let app = shallow( );
expect(app.find('h1').text())
.to.equal('Available Products');
});
});
describe('Enzyme Mount', function () {
it('Delete An Item', function () {
let app = mount( );
let itemLength = app.find('li').length;
app.find('button.delete').at(0).simulate('click');
expect(app.find('li').length).to.equal(itemLength - 1);
});
});
App.css:用于美化项目
应用程序.css
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #006400;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.main-wrapper {
display: flex;
justify-content: center;
}
table {
margin: 1rem;
}
table th td,
table tr td {
border: 1px solid black;
border-collapse: collapse;
}
form {
margin: 1rem;
}
input {
line-height: 1.4rem;
margin-left: 1rem;
}
h5 {
display: block;
}
form button {
line-height: 1.4rem;
background-color: #ffffff;
cursor: pointer;
}
.cities-wrapper {
border: 1px solid black;
margin: 1rem;
padding: 1rem;
align-items: flex-end;
height: fit-content;
}
ul {
float: center;
margin-top: 0;
}
.center {
margin-left: auto;
margin-right: auto;
}
.shelf-wrapper {
text-align: left;
display: flex;
justify-content: center;
}
.shelf-wrapper h4 {
text-align: center;
}
.shelf-wrapper .shelf {
width: 19%;
border: 1px solid black;
display: inline-block;
min-height: 15rem;
}
.shelf span {
/* width: 60%; */
}
.shelf button {
float: right;
/* width: 40%; */
}
.book-wrapper span {
width: 100%;
}
.shelf table tr td {
border: none;
}
ItemJSON.js:由于本项目不涉及任何数据库,所以从“ItemJSON.js”中挑选项目。它位于 src >> app >> Items >> ItemJSON.js
ItemJSON.js
import { EventEmitter } from 'events';
import assign from 'object-assign';
// Initially specifying the constant items just as an sample
const ProductStore = assign({}, EventEmitter.prototype, {
items: {
products: [
{ productId: 0, productName: 'Samsung',
productPrice: 10000, productQuantity: 2 },
{ productId: 1, productName: 'Motorola',
productPrice: 7000, productQuantity: 3 },
{ productId: 2, productName: 'Redmi',
productPrice: 8000, productQuantity: 4 },
]
},
nextproductId: 3,
// To get all the items and display in the screen
getAll: function getAll() {
return this.items;
},
emitChange: function emitChange() {
this.emit('change');
},
// When an item is added
addChangeListener: function addChangeListener(callback) {
this.on('change', callback);
},
// When an item is removed
removeChangeListener: function removeChangeListener(callback) {
this.removeListener('change', callback);
},
addNewProducts: function addNewProducts(product) {
const products = this.items.products;
if (!products ||
typeof this.items.products.length !== 'number') {
this.items.products = [];
}
product.productId = this.nextproductId++;
product.done = false;
this.items.products.push(product);
},
deleteProducts: function deleteProducts(productId) {
this.items.products = this.items.products.filter(
(product) => product.productId !== productId);
}
});
export default ProductStore;
启动应用程序:编写以下命令来启动应用程序。项目从3000端口开始
npm start
输出:
接下来,可以从 App.jsx 中找到所需的代码
应用程序.jsx
import React from 'react';
import AddItems from './AddItems';
import List from './List';
export default class App extends React.Component {
render() {
return (
Available Products
// List.jsx is enclosed
);
}
}
List.jsx:我们可以选择添加项目以及删除项目
列表.jsx
import React from 'react';
import ItemJSON from '../Items/ItemJSON';
import ListItems from './ListItems';
export default class ProductList extends React.Component {
constructor(props) {
super(props);
this.state = ItemJSON.getAll();
}
componentDidMount() {
ItemJSON.addChangeListener(this._onChange.bind(this));
}
componentWillUnmount() {
ItemJSON.removeChangeListener(this._onChange.bind(this));
}
_onChange() {
this.setState(ItemJSON.getAll());
}
render() {
const ListItemsList = this.state.products.map(
product => {
return (
);
});
return (
// All the items present in
// ItemJSON.js is displayed here
{ListItemsList}
);
}
}
输出:
输入产品详细信息并单击“添加”按钮后,会出现以下功能:
Javascript
import React from 'react';
import ItemJSON from '../Items/ItemJSON';
export default class AddItems extends React.Component {
//will pick the added product and added
addItems() {
const newProductName = this.refs.product.value;
const newPrice = this.refs.price.value;
const newQuantity = this.refs.quantity.value;
if (newProductName) {
ItemJSON.addNewProducts({
productName: newProductName,
productPrice: newPrice,
productQuantity: newQuantity
});
ItemJSON.emitChange();
this.refs.product.value = '';
this.refs.price.value = '';
this.refs.quantity.value = '';
}
}
render() {
return (
Product Name
Price
Quantity
Action
);
}
}
删除产品:让我们尝试从上面的列表中删除 Redmi。删除所需的代码在 ListItems.jsx 中
ListItems.jsx
import React from 'react';
import ItemJSON from '../Items/ItemJSON';
export default class ListItems extends React.Component {
// This code is meant for deletion
deleteProduct(e) {
e.preventDefault();
ItemJSON.deleteProducts(this.props.product.productId);
ItemJSON.emitChange();
}
render() {
const product = this.props.product;
return (
// displaying available products and it
// is having delete action
{product.productName}
{product.productPrice}
{product.productQuantity}
);
}
}
输出:
我们可以测试项目的功能如下:
应用程序.test.js
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import { expect } from 'chai';
import { shallow, mount, configure } from 'enzyme';
import TestUtils from 'react-dom/test-utils';
import App from './app/components/App';
import jsdom from 'jsdom';
import { findDOMNode } from 'react-dom';
configure({ adapter: new Adapter() });
beforeAll(() => {
global.fetch = jest.fn();
// window.fetch = jest.fn(); if running browser environment
});
let wrapper;
beforeEach(() => {
wrapper = shallow(< App />, { disableLifecycleMethods: true });
});
afterEach(() => {
wrapper.unmount();
});
if (typeof document === 'undefined') {
global.document = jsdom.jsdom(
'');
global.window = document.defaultView;
global.navigator = global.window.navigator;
}
describe('DOM Rendering', function () {
it('Add functionality to add new products
by clicking add', function () {
const app = TestUtils.renderIntoDocument( );
const appDOM = findDOMNode(app);
let productItemsLength =
appDOM.querySelectorAll('.todo-text').length;
let addInput = appDOM.querySelector('input');
addInput.value = 'Add item';
let addButton = appDOM.querySelector('.add-todo button');
TestUtils.Simulate.click(addButton);
console.log(appDOM.querySelectorAll('.todo-text').length);
expect(appDOM.querySelectorAll('.todo-text')
.length).to.be.equal(productItemsLength + 3);
// As after adding we will get additional value 3.
});
});
describe('DOM Rendering', function () {
it('On deleteing, the item should get deleted', function () {
const app = TestUtils.renderIntoDocument( );
let productItems = TestUtils
.scryRenderedDOMComponentsWithTag(app, 'li');
let productLength = productItems.length;
let deleteButton = productItems[0]
.querySelector('button');
TestUtils.Simulate.click(deleteButton);
let productItemsAfterClick = TestUtils
.scryRenderedDOMComponentsWithTag(app, 'li');
expect(productItemsAfterClick.length)
.to.equal(productLength - 1);
});
});
describe('Enzyme Shallow', function () {
it('App\'s title should be Available Products', function () {
let app = shallow( );
expect(app.find('h1').text())
.to.equal('Available Products');
});
});
describe('Enzyme Mount', function () {
it('Delete An Item', function () {
let app = mount( );
let itemLength = app.find('li').length;
app.find('button.delete').at(0).simulate('click');
expect(app.find('li').length).to.equal(itemLength - 1);
});
});
可以通过以下方式测试测试脚本:
npm test
输出: