progressbar.js icon indicating copy to clipboard operation
progressbar.js copied to clipboard

JSdom causing Error: Uncaught [TypeError: this.path.getTotalLength is not a function]

Open cybersmithio opened this issue 2 years ago • 0 comments

In a React project that uses Jest and Testing Library, after adding some ProgressBar circles, the tests started to fail. I believe it is due to JSDom not supporting SVG.

The error I'm receiving is:

  console.error
    Error: Uncaught [TypeError: this.path.getTotalLength is not a function]
        at reportException (node_modules\jsdom\lib\jsdom\living\helpers\runtime-script-errors.js:66:24)
        at innerInvokeEventListeners (node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:341:9)
        at invokeEventListeners (node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
        at HTMLUnknownElementImpl._dispatch (node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
        at HTMLUnknownElementImpl.dispatchEvent (node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:94:17)
        at HTMLUnknownElement.dispatchEvent (node_modules\jsdom\lib\jsdom\living\generated\EventTarget.js:231:34)
        at Object.invokeGuardedCallbackDev (node_modules\react-dom\cjs\react-dom.development.js:3994:16)
        at invokeGuardedCallback (node_modules\react-dom\cjs\react-dom.development.js:4056:31)
        at beginWork$1 (node_modules\react-dom\cjs\react-dom.development.js:23964:7)
        at performUnitOfWork (node_modules\react-dom\cjs\react-dom.development.js:22779:12)
        at workLoopSync (node_modules\react-dom\cjs\react-dom.development.js:22707:5)
        at renderRootSync (node_modules\react-dom\cjs\react-dom.development.js:22670:7)
        at performSyncWorkOnRoot (node_modules\react-dom\cjs\react-dom.development.js:22293:18)
        at node_modules\react-dom\cjs\react-dom.development.js:11327:26
        at unstable_runWithPriority (node_modules\scheduler\cjs\scheduler.development.js:468:12)
        at runWithPriority$1 (node_modules\react-dom\cjs\react-dom.development.js:11276:10)
        at flushSyncCallbackQueueImpl (node_modules\react-dom\cjs\react-dom.development.js:11322:9)
        at flushSyncCallbackQueue (node_modules\react-dom\cjs\react-dom.development.js:11309:3)
        at scheduleUpdateOnFiber (node_modules\react-dom\cjs\react-dom.development.js:21893:9)
        at setProgress (node_modules\react-dom\cjs\react-dom.development.js:16139:5)
        at _callee$ (src\components\widget\IamProgressWidget\IamProgressWidget.js:28:7)
        at tryCatch (node_modules\@babel\runtime\helpers\regeneratorRuntime.js:86:17)
        at Generator._invoke (node_modules\@babel\runtime\helpers\regeneratorRuntime.js:66:24)
        at Generator.next (node_modules\@babel\runtime\helpers\regeneratorRuntime.js:117:21)
        at asyncGeneratorStep (node_modules\@babel\runtime\helpers\asyncToGenerator.js:3:24)
        at _next (node_modules\@babel\runtime\helpers\asyncToGenerator.js:25:9)
        at runNextTicks (node:internal/process/task_queues:61:5)
        at listOnTimeout (node:internal/timers:526:9)
        at processTimers (node:internal/timers:500:7) TypeError: this.path.getTotalLength is not a function
        at new Path (node_modules\progressbar.js\src\path.js:43:28)
        at Circle.Shape (node_modules\progressbar.js\src\shape.js:99:26)
        at new Circle (node_modules\progressbar.js\src\circle.js:16:11)
        at src\components\widget\IamProgressWidget\ProgressCircle.js:23:7
        at mountMemo (node_modules\react-dom\cjs\react-dom.development.js:15846:19)
        at Object.useMemo (node_modules\react-dom\cjs\react-dom.development.js:16219:16)
        at useMemo (node_modules\react\cjs\react.development.js:1532:21)
        at ProgressCircle (src\components\widget\IamProgressWidget\ProgressCircle.js:21:22)
        at renderWithHooks (node_modules\react-dom\cjs\react-dom.development.js:14985:18)
        at mountIndeterminateComponent (node_modules\react-dom\cjs\react-dom.development.js:17811:13)
        at beginWork (node_modules\react-dom\cjs\react-dom.development.js:19049:16)
        at HTMLUnknownElement.callCallback (node_modules\react-dom\cjs\react-dom.development.js:3945:14)
        at HTMLUnknownElement.callTheUserObjectsOperation (node_modules\jsdom\lib\jsdom\living\generated\EventListener.js:26:30)
        at innerInvokeEventListeners (node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:338:25)
        at invokeEventListeners (node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
        at HTMLUnknownElementImpl._dispatch (node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
        at HTMLUnknownElementImpl.dispatchEvent (node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:94:17)
        at HTMLUnknownElement.dispatchEvent (node_modules\jsdom\lib\jsdom\living\generated\EventTarget.js:231:34)
        at Object.invokeGuardedCallbackDev (node_modules\react-dom\cjs\react-dom.development.js:3994:16)
        at invokeGuardedCallback (node_modules\react-dom\cjs\react-dom.development.js:4056:31)
        at beginWork$1 (node_modules\react-dom\cjs\react-dom.development.js:23964:7)
        at performUnitOfWork (node_modules\react-dom\cjs\react-dom.development.js:22779:12)
        at workLoopSync (node_modules\react-dom\cjs\react-dom.development.js:22707:5)
        at renderRootSync (node_modules\react-dom\cjs\react-dom.development.js:22670:7)
        at performSyncWorkOnRoot (node_modules\react-dom\cjs\react-dom.development.js:22293:18)
        at node_modules\react-dom\cjs\react-dom.development.js:11327:26
        at unstable_runWithPriority (node_modules\scheduler\cjs\scheduler.development.js:468:12)
        at runWithPriority$1 (node_modules\react-dom\cjs\react-dom.development.js:11276:10)
        at flushSyncCallbackQueueImpl (node_modules\react-dom\cjs\react-dom.development.js:11322:9)
        at flushSyncCallbackQueue (node_modules\react-dom\cjs\react-dom.development.js:11309:3)
        at scheduleUpdateOnFiber (node_modules\react-dom\cjs\react-dom.development.js:21893:9)
        at setProgress (node_modules\react-dom\cjs\react-dom.development.js:16139:5)
        at _callee$ (src\components\widget\IamProgressWidget\IamProgressWidget.js:28:7)
        at tryCatch (node_modules\@babel\runtime\helpers\regeneratorRuntime.js:86:17)
        at Generator._invoke (node_modules\@babel\runtime\helpers\regeneratorRuntime.js:66:24)
        at Generator.next (node_modules\@babel\runtime\helpers\regeneratorRuntime.js:117:21)
        at asyncGeneratorStep (node_modules\@babel\runtime\helpers\asyncToGenerator.js:3:24)
        at _next (node_modules\@babel\runtime\helpers\asyncToGenerator.js:25:9)
        at runNextTicks (node:internal/process/task_queues:61:5)
        at listOnTimeout (node:internal/timers:526:9)
        at processTimers (node:internal/timers:500:7)

The React widget code to draw the circle is below:

import React, { useEffect, useMemo, useCallback } from "react";
import { Circle } from "progressbar.js";

const ProgressCircle = ({ level, animate }) => {
  var wrapper = document.createElement("div");
  wrapper.className = "progressBarDiv";
  wrapper.style = "height:50px;display:flex;flex-direction:column";
  wrapper.id = level;
  var percentage = Math.round(animate * 100);
  var color = "#7F00FF";
  if (percentage >= 80) {
    color = "#00FF00";
  } else if (percentage >= 50) {
    color = "#003FFF";
  }

  const bar = useMemo(
    () =>
      new Circle(wrapper, {
        strokeWidth: 6,
        easing: "easeInOut",
        duration: 1400,
        color: color,
        trailColor: "#888",
        trailWidth: 1,
        step: function (state, circle) {
          circle.setText(percentage + "%");
        },
      }),
    []
  );

  const node = useCallback((node) => {
    if (node) {
      node.appendChild(wrapper);
    }
  }, []);

  useEffect(() => {
    bar.animate(animate);
  }, [animate, bar]);

  return <div ref={node} />;
};

export default ProgressCircle;

As a workaround, I added this code to check if the userAgent has "jsdom" in it, and just return a blank div instead of drawing.

  // Don't try to draw anything if the user agent reports "jsdom",
  // since Jest/Testing Library using jsdom cannot draw SVGs and this breaks the
  // ProgressBar.js library.
  var userAgent = window.navigator.userAgent;
  if (userAgent !== undefined) {
    if (userAgent.search("jsdom") >= 0) {
      return <div />;
    }
  }

Would it be possible to add some validation on the path value. In this case, it was the in the Path function in path.js:

var Path = function Path(path, opts) {
    // Throw a better error if not initialized with `new` keyword
    if (!(this instanceof Path)) {
        throw new Error('Constructor was called without new keyword');
    }

    // Default parameters for animation
    opts = utils.extend({
        delay: 0,
        duration: 800,
        easing: 'linear',
        from: {},
        to: {},
        step: function() {}
    }, opts);

    var element;
    if (utils.isString(path)) {
        element = document.querySelector(path);
    } else {
        element = path;
    }

    // Reveal .path as public attribute
    this.path = element;
    this._opts = opts;
    this._tweenable = null;

    // Set up the starting positions
    var length = this.path.getTotalLength();
    this.path.style.strokeDasharray = length + ' ' + length;
    this.set(0);
};

cybersmithio avatar Dec 02 '22 14:12 cybersmithio