0

I'm trying to put our SPFx React Components under test.
We're using following technologies:

  • spfx 1.13.1
  • react 16.13.1
  • jest 27.4.7
  • enzyme 3.11.0
  • typescript 4.2.4
  • fabric-ui: 1.13.1

I am stuck on following error when running a simple test using enzym.mount.

TypeError: window.requestAnimationFrame is not a function

  at Stylesheet.Object.<anonymous>.Stylesheet._getStyleElement (node_modules/@uifabric/merge-styles/src/Stylesheet.ts:295:16)
  at Stylesheet.Object.<anonymous>.Stylesheet.insertRule (node_modules/@uifabric/merge-styles/src/Stylesheet.ts:227:65)
  at applyRegistration (node_modules/@uifabric/merge-styles/src/styleToClassName.ts:284:20)
  at Object.styleToClassName (node_modules/@uifabric/merge-styles/src/styleToClassName.ts:294:5)
  at mergeCss (node_modules/@uifabric/merge-styles/src/mergeStyles.ts:30:18)
  at Object.mergeStyles (node_modules/@uifabric/merge-styles/src/mergeStyles.ts:13:10)
  at Object.<anonymous> (node_modules/@uifabric/utilities/src/scroll.ts:9:33)

Based on various other answers (here, here), I tried to play around with following code

global.requestAnimationFrame = function (callback) {
    return setTimeout(callback, 0);
};

placing it at top of my test code, inside the describe and in a separate test-init file.

I also tried to use ras/polyfill but without any change in the error.

Any help in moving me forward to a working solution?

test file.

/// <reference types="jest" />

import * as React from 'react';
import { configure, mount, ReactWrapper, shallow } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
import "jsdom-global/register";

import MyComponent, { IMyComponentProps, IMyComponentState } from './my-component';

// import { polyfill } from 'raf';
// polyfill();

// global.requestAnimationFrame = function (callback) {
//     return setTimeout(callback, 0);
// };

configure({ adapter: new Adapter() });

describe('<MyComponent/>', () => {
    let renderedElement: ReactWrapper<IMyComponentProps, IMyComponentState>;

    beforeEach(() => {
        renderedElement = mount(<MyComponent/>);
    });

    afterEach(() => {
        renderedElement.unmount();
    });

    it('load my component', () => {
        expect(renderedElement).toBeTruthy();
    });

});

component controls

<TextField name='abc' label="" defaultValue={state.abc} onChange={_handleChange} />
<Dropdown label='xyz' placeholder='Select xyz' options={options} defaultSelectedKey={state.xyz} onChange={(e, i) => _handleChangeDropdown(e, i, 'xyz')} />

package.json file

  ...
  "devDependencies": {
    "@microsoft/microsoft-graph-types": "^2.11.0",
    "@microsoft/rush-stack-compiler-4.2": "0.1.1",
    "@microsoft/sp-build-web": "1.13.1",
    "@microsoft/sp-module-interfaces": "1.13.1",
    "@types/enzyme": "^3.10.11",
    "@types/jest": "^27.4.0",
    "@types/node": "^17.0.10",
    "@types/react": "17.0.38",
    "@types/react-dom": "17.0.11",
    "@types/react-router-dom": "^5.3.2",
    "@types/webpack-env": "1.16.3",
    "@typescript-eslint/eslint-plugin": "^5.9.1",
    "@typescript-eslint/parser": "^5.9.1",
    "ajv": "~8.8.2",
    "enzyme": "^3.11.0",
    "enzyme-adapter-react-16": "^1.15.6",
    "eslint": "^8.6.0",
    "eslint-config-react-app": "^7.0.0",
    "eslint-plugin-react": "^7.28.0",
    "gulp": "~4.0.2",
    "gulp-eslint-new": "^1.1.1",
    "identity-obj-proxy": "^3.0.0",
    "jest": "^27.4.7",
    "jest-junit": "^13.0.0",
    "jsdom": "^19.0.0",
    "jsdom-global": "^3.0.2",
    "raf": "^3.4.1",
    "react-test-renderer": "^17.0.2",
    "spfx-fast-serve-helpers": "~1.13.0",
    "ts-jest": "^27.1.3",
    "typescript": "4.2.4"
},
"jest": {
    "moduleFileExtensions": [
        "ts",
        "tsx",
        "js"
    ],
    "moduleDirectories": [
        "node_modules"
    ],
    "moduleNameMapper": {
        "\\.(css|less|scss|sass)$": "identity-obj-proxy",
        "office-ui-fabric-react/lib/(.*)$": "office-ui-fabric-react/lib-commonjs/$1",
        "^@mywebpart/(.*)": "<rootDir>/src/webparts/mywebpart/$1",
        "^@shared/(.*)": "<rootDir>/src/shared/$1"
    },
    "transform": {
        "^.+\\.(ts|tsx)$": "ts-jest"
    },
    "testMatch": [
        "**/src/**/*.tests.+(ts|tsx)"
    ],
    "setupFiles": [
        "<rootDir>/src/webparts/mywebpart/tests/tests-setup.ts"
    ],
    "collectCoverage": true,
    "coverageReporters": [
        "json",
        "lcov",
        "text",
        "cobertura"
    ],
    "coverageDirectory": "<rootDir>/test-results",
    "reporters": [
        "default",
        "jest-junit"
    ],
    "coverageThreshold": {
        "global": {
            "branches": 60,
            "functions": 60,
            "lines": 60,
            "statements": 60
        }
    }
},
"jest-junit": {
    "output": "./test-results/summary-jest-junit.xml"
}

tsconfig.json file

{
  "extends": "./node_modules/@microsoft/rush-stack-compiler-4.2/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,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "typeRoots": [
      "./node_modules/@types",
      "./node_modules/@microsoft"
    ],
    "types": [
      "webpack-env",
      "jest",
      "node"
    ],
    "lib": [
      "es6",
      "dom",
      "es2015.collection",
      "es2015.promise"
    ],
    "baseUrl": "src",
    "paths": {
      "@mywebpart/*": [
        "webparts/mywebpart/*"
      ],
      "@shared/*": [
        "shared/*"
      ]
    }
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx"
  ]
}
Ronald
  • 1,990
  • 6
  • 24
  • 39

1 Answers1

0

I was able to solve the issue, though I'm not 100% sure I can follow the flow.

I created a tests setup file configured with jest.

    "setupFiles": [
        "<rootDir>/src/webparts/my-part/tests/tests-setup.ts"
    ],

File content.

/// <reference types="jest" />

import "jsdom-global/register";
// Polyfill requestAnimiationFrame
import "raf/polyfill";

import { configure } from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';
import { setIconOptions } from "office-ui-fabric-react/lib/Styling";

// Suppress icon warnings.
setIconOptions({
    disableWarnings: true
});

// Fail on warnings.
const consoleError = console.error;
console.error = console.warn = (message) => {
    // Ignore error boundary warnings so that tests which throw exceptions can be validated.
    if (message.indexOf('error boundary') >= 0) {
        return;
    }

    consoleError(message);
    throw new Error("Caught: " + message);
};

// configure enzyme adapter
configure({ adapter: new Adapter() });
Ronald
  • 1,990
  • 6
  • 24
  • 39