如何在 SharePoint SPFx + react 项目中配置 jest 单元测试框架

Posted

技术标签:

【中文标题】如何在 SharePoint SPFx + react 项目中配置 jest 单元测试框架【英文标题】:how to configure jest unit test framework in SharePoint SPFx + react project 【发布时间】:2021-12-24 16:50:15 【问题描述】:

我尝试配置 jest 单元测试框架以响应 SharePoint SPFx 项目

下面我提到所有与 jest 配置和测试用例相关的代码以及

在 react + gulp 中为 SharePoint SPFx 配置项目

package.json


  "name": "pnp-sp-crud",
  "version": "0.0.1",
  "private": true,
  "main": "lib/index.js",
  "engines": 
    "node": ">=0.10.0"
  ,
  "scripts": 
    "build": "gulp bundle",
    "clean": "gulp clean",
    "test": "jest"
  ,
  "dependencies": 
    "@microsoft/sp-core-library": "1.10.0",
    "@microsoft/sp-lodash-subset": "1.10.0",
    "@microsoft/sp-office-ui-fabric-core": "1.10.0",
    "@microsoft/sp-property-pane": "1.10.0",
    "@microsoft/sp-webpart-base": "1.10.0",
    "@pnp/common": "^2.0.3",
    "@pnp/sp": "^2.0.3",
    "@testing-library/jest-dom": "^5.15.0",
    "@testing-library/react": "^12.1.2",
    "@types/es6-promise": "0.0.33",
    "@types/react": "16.8.8",
    "@types/react-dom": "16.8.3",
    "@types/webpack-env": "1.13.1",
    "jest-cli": "^27.3.1",
    "office-ui-fabric-react": "6.189.2",
    "react": "^16.8.5",
    "react-block-ui": "^1.3.3",
    "react-dom": "^16.8.5"
  ,
  "resolutions": 
    "@types/react": "16.8.8"
  ,
  "devDependencies": 
    "@microsoft/rush-stack-compiler-3.3": "0.3.5",
    "@microsoft/sp-build-web": "1.10.0",
    "@microsoft/sp-module-interfaces": "1.10.0",
    "@microsoft/sp-tslint-rules": "1.10.0",
    "@microsoft/sp-webpart-workbench": "1.10.0",
    "@types/chai": "3.4.34",
    "@types/jest": "^27.0.2",
    "@types/mocha": "2.2.38",
    "ajv": "~5.2.2",
    "gulp": "~3.9.1",
    "jest": "^27.3.1",
    "ts-jest": "^27.0.7",
    "typescript": "^4.4.4"
  


jest.config.js

module.exports = 
    transform: 
        "^.+\\.(ts|tsx)$": "ts-jest",
    ,
    coveragePathIgnorePatterns: [
        "/node_modules/"
    ],
    testRegex: [
        '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$'
    ]
;

tsconfig.json


  "extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
  "compilerOptions": 
    "target": "es5",
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "jsx": "react",
    "declaration": true,
    "sourceMap": true,
    "experimentalDecorators": true,
    "skipLibCheck": true,
    "outDir": "lib",
    "inlineSources": false,
    "strictNullChecks": false,
    "noUnusedLocals": false,
    "typeRoots": [
      "./node_modules/@types",
      "./node_modules/@microsoft"
    ],
    "types": [
      "es6-promise",
      "webpack-env",
      "jest"
    ],
    "lib": [
      "es5",
      "dom",
      "es2015.collection"
    ]
  ,
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "lib"
  ]

PnpSpCrud.tsx

import * as React from 'react';
import styles from './PnpSpCrud.module.scss';
import  IPnpSpCrudProps  from './IPnpSpCrudProps';
import  escape  from '@microsoft/sp-lodash-subset';
require("@pnp/logging");
require("@pnp/common");
require("@pnp/odata");
import  sp  from "@pnp/sp/presets/all";
import BlockUi from 'react-block-ui';

export default class PnpSpCrud extends React.Component<IPnpSpCrudProps, 
  name: any,
  tech: any,
  employees: any[],
  isUpdate: boolean,
  empId: any,
  empIndex: any,
  blocking: boolean
> 

  constructor(props) 
    super(props);
    this.state = 
      name: '',
      tech: '',
      employees: [],
      isUpdate: false,
      empId: '',
      empIndex: '',
      blocking: false
    ;

    this.getEmployee();
    this.enterName = this.enterName.bind(this);
    this.enterTechnology = this.enterTechnology.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  

  private enterName(event) 
    this.setState( name: event.target.value );
  

  private enterTechnology(event) 
    this.setState( tech: event.target.value );
  

  private handleSubmit(event) 
    this.addEmployee(this.state);
    event.preventDefault();
  

  public render(): React.ReactElement<IPnpSpCrudProps> 
    return (
      <BlockUi tag="div" blocking=this.state.blocking>
        <div className=styles.pnpSpCrud>
          <div className=styles.container>
            <div className=styles.row>
              <div className=styles.column>
                /* <span className=styles.title>Welcome to SharePoint!</span>
              <p className=styles.subTitle>Customize SharePoint experiences using Web Parts.</p>
              <p className=styles.description>escape(this.props.description)</p> */
                <h4>Add Employee</h4>
                <form>
                  <div className="row">
                    <div className="col-md-6">
                      <div className="form-group">
                        <label>Employee Name</label>
                        <input type="text" className="form-control" placeholder="Enter name" value=this.state.name onChange=this.enterName />
                      </div>
                    </div>
                    <div className="col-md-6">
                      <div className="form-group">
                        <label>Technology</label>
                        <input type="text" className="form-control" placeholder="Enter Technology" value=this.state.tech onChange=this.enterTechnology />
                      </div>
                    </div>
                    /* <input type="text" placeholder="Employee Name" value=this.state.name onChange=this.enterName></input>
                  <input type="text" style= marginLeft: 5  placeholder="Technology" value=this.state.tech onChange=this.enterTechnology></input>
                  <div style= marginTop: 5 >
                    <input type="submit" value="Submit" />
                  </div> */
                  </div>
                </form>
                <button className="btn btn-primary mb-5" onClick=this.handleSubmit>Submit</button>
                <h4>Employees</h4>
                <table className="table table-dark">
                  <tbody>
                    <tr>
                      <th>#</th>
                      <th>Name</th>
                      <th colSpan=2>Technology</th>
                    </tr>
                    
                      this.state.employees.map((ele, index) => 
                        return (
                          <tr>
                            <td>index + 1</td>
                            <td>ele.Title</td>
                            <td>ele.tech</td>
                            <td style= color: 'while', cursor: 'pointer' ><i className="fa fa-pencil" aria-hidden="true" onClick=() => this.updateEmployee(ele.ID, index)></i></td>
                            <td style= color: 'while', cursor: 'pointer' ><i className="fa fa-trash" style= color: 'while'  aria-hidden="true" onClick=() => this.deleteEmployee(ele.ID, index)></i></td>
                          </tr>
                        );
                      )
                    
                  </tbody>
                </table>
                /* <a href="https://aka.ms/spfx" className=styles.button>
                <span className=styles.label>Learn more</span>
              </a> */
              </div>
            </div>
          </div>
        </div>
      </BlockUi>
    );
  

  private getEmployee() 
    return new Promise((resolve, reject) => 
      sp.web.lists.getByTitle('EmployeeList').items.select('ID, Title, tech').get().then((result: any[]) => 
        console.log("get employees result", result);
        resolve(result);
        this.setState(
          employees: result
        );
      ).catch((err) => 
        console.log("error in get employees", err);
      );
    );
  

  private addEmployee(state) 
    if (this.state.name && this.state.tech) 
      if (this.state.isUpdate) 
        this.setState( blocking: true );
        return new Promise((resolve, reject) => 
          let data = 
            Title: state.name,
            tech: state.tech
          ;
          sp.web.lists.getByTitle('EmployeeList').items.getById(this.state.empId).update(data).then((result: any) => 
            resolve(result);
            alert('Employee Updated');
            this.state.employees[this.state.empIndex].Title = data.Title;
            this.state.employees[this.state.empIndex].tech = data.tech;
            this.setState( name: '', tech: '', isUpdate: false, empId: '', empIndex: '', blocking: false );
          ).catch((err) => 
            alert('Employee Not Updated');
            this.setState( blocking: false );
            console.log("error in update employee", err);
          );
        );
      
      else 
        this.setState( blocking: true );
        return new Promise((resolve, reject) => 
          let data = 
            Title: state.name,
            tech: state.tech
          ;
          sp.web.lists.getByTitle('EmployeeList').items.add(data).then((result: any) => 
            console.log("result from add employee", result);
            resolve(result);
            alert('Employee added');
            this.state.employees.push(
              ID: result.data.ID,
              Title: state.name,
              tech: state.tech
            );
            this.setState( name: '', tech: '', blocking: false );
          ).catch((err) => 
            alert('Employee not added');
            this.setState( blocking: false );
            console.log("error in add employee", err);
          );
        );
      
    
    else 
      alert("Enter Name and Technology");
    
  

  private updateEmployee(id, index) 
    this.setState( blocking: true );
    return new Promise((resolve, reject) => 
      sp.web.lists.getByTitle('EmployeeList').items.getById(id).get().then((result: any) => 
        console.log("get employee by id result", result);
        resolve(result);
        this.setState(
          name: result.Title,
          tech: result.tech,
          isUpdate: true,
          empId: id,
          empIndex: index,
          blocking: false
        );
      ).catch((err) => 
        this.setState( blocking: false );
        console.log("error in get employee by id", err);
      );
    );
  

  private deleteEmployee(id, index) 
    this.setState( blocking: true );
    return new Promise((resolve, reject) => 
      sp.web.lists.getByTitle('EmployeeList').items.getById(id).delete().then((result: any) => 
        console.log("delete employee by id result", result);
        resolve(result);
        alert("Employee deleted");
        this.state.employees.splice(index, 1);
        this.setState(
          empId: '',
          empIndex: '',
          blocking: false
        );
      ).catch((err) => 
        this.setState( blocking: false );
        console.log("error in delete employee by id", err);
      );
    );
  


PnpSpCrud.spec.tsx

import PnpSpCrud from '../PnpSpCrud';

test('renders learn react link', () => 
    expect(PnpSpCrud).toBeTruthy();
);

Error while run the unit test

【问题讨论】:

【参考方案1】:

据我所知,SPFx 目前尚未正式支持 jest(或任何其他单元测试框架)。

我遇到了类似的问题,请查看 GitHub 上的这个帖子,讨论如何让 jest 与 SPFx 和 PNP 一起工作: https://github.com/pnp/pnpjs/issues/1425

这是带有工作配置的演示仓库: https://github.com/nbelyh/spfx-unit-testing-pnpjs

【讨论】:

感谢@nikolay 的回复。我找到了另一种解决方案,您也可以尝试在 SPFx 项目中配置 jest:github.com/Voitanos/jest-preset-spfx-react16 上面帖子中提到过,请查看与它相关的这些cmets:github.com/pnp/pnpjs/issues/1425#issuecomment-721110960 知道了。您能否也看看另一个相同的问题:***.com/questions/69973511/… 请阅读上面链接的评论,它似乎解决了您面临的问题。 我看到了你的 GitHub 问题。但是对于Import PNP没有任何解决方案。我还查看了您的存储库:spfx-unit-testing-pnpjs。但就我而言,我需要对 SharePoint 列表和 SPFx 组件使用导入。谢谢

以上是关于如何在 SharePoint SPFx + react 项目中配置 jest 单元测试框架的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 SharePoint SPFX webpart 构建团队应用程序

如何在SPFX经典视图中创建自定义母版页

如何在 SharePoint SPFx + react 项目中配置 jest 单元测试框架

使用 SPFx 扩展将自定义操作部署到 SharePoint 2019 列表

在 SharePoint 框架 (SPFX) / Angular 中使用 ag-Grid - 样式失败

无法通过 App Catalog 将 SPFx webpart 部署到 sharepoint 2019