nus-mtp/sashimi-note

View on GitHub
sashimi-webapp/test/unit/specs/logic/diagramsRenderer.spec.js

Summary

Maintainability
F
3 days
Test Coverage
import domCompare from 'dom-compare';
import DiagramsRenderer from 'src/logic/renderer/diagrams';
import documentPackager from 'src/logic/documentPackager';
import diagramsInput from './reference/diagrams/diagramsInput.txt';
import diagramsRenderedOutput from './reference/diagrams/diagramsRenderedOutput.txt';
import flowChartsInput from './reference/diagrams/flowChartsInput.txt';
import flowChartsOutput from './reference/diagrams/flowChartsOutput.txt';
import graphvizInput from './reference/diagrams/graphvizInput.txt';
import graphvizOutput from './reference/diagrams/graphvizOutput.txt';
import invalidSyntaxInput from './reference/diagrams/invalidSyntaxInput.txt';
import invalidSyntaxOutput from './reference/diagrams/invalidSyntaxOutput.txt';
import mermaidInput from './reference/diagrams/mermaidInput.txt';
import mermaidOutput from './reference/diagrams/mermaidOutput.txt';
import seqDiagramsInput from './reference/diagrams/seqDiagramsInput.txt';
import seqDiagramsOutput from './reference/diagrams/seqDiagramsOutput.txt';

// Parse markdown content for test use
const fullHtmlData = documentPackager.getHtmlData(diagramsInput);
const seqHtmlData = documentPackager.getHtmlData(seqDiagramsInput);
const flowHtmlData = documentPackager.getHtmlData(flowChartsInput);
const vizHtmlData = documentPackager.getHtmlData(graphvizInput);
const mermaidHtmlData = documentPackager.getHtmlData(mermaidInput);
const invalidHtmlData = documentPackager.getHtmlData(invalidSyntaxInput);

// Map data structure to help with Error checking
const textExpected = new Map();
const textActual = new Map();

// Use iframe as a psuedo browser for testing
const iframe = document.createElement('IFRAME');
let iframeDoc;
let toRender;

// DiagramsRender Object
let diagramsRenderer;

/**
 * Helper function for checking for 'Expected text' Error (See case 4 in regex helper below)
 * @param {String} path - String containing Error Node's path
 * @param {String} expected - Expected HTML string output
 * @param {Element} actual - HTML element containing rendered(actual) output
 */
function helper(path, expected, actual) {
  // Change path into CSS selector format
  const key = path;
  path = path.replace(/\//g, ' ');
  function addRemove(match, captured) {
    return `:nth-of-type(${captured})`;
  }
  path = path.replace(/\[([0-9]*)\]/g, addRemove);

  // Remove carriage return from string
  expected = expected.replace(/(\r\n)/g, '\n');
  // Prepare the expected output as an HTML Element for use by helper function
  const expectedHTML = iframeDoc.createElement('DIV');
  expectedHTML.innerHTML = expected;

  const expArr = expectedHTML.querySelector(path).getElementsByTagName('tspan');
  const actArr = actual.querySelector(path).getElementsByTagName('tspan');
  let expStr = '';
  let actStr = '';

  for (let i = 0; i < expArr.length; i+=1) {
    expStr.concat(`${expArr[i].innerText} `);
  }
  for (let i = 0; i < actArr.length; i+=1) {
    actStr.concat(`${actArr[i].innerText} `);
  }
  expStr = expStr.trim();
  actStr = actStr.trim();
  textExpected.set(key, expStr);
  textActual.set(key, actStr);
  return (expStr === actStr);
}

/**
 * Regex helper function for checking differences found by domCompare library.
 *
 * We are using this because some attribute values/styling are rendered differently on travis,
 * causing travis to keep failing, although local unit test passes the test.
 *
 * Four cases to ignore:
 * 1. Attribute value differs by parenthesis even though its the same font or because of random generation of value
 *  regex1 case:
 *    /(.*) '(.*)':.* '(.*)'.*'(.*)'/g;
 *    1st (.*) captures the category causing the Error (e.g. is it Tag, Attribute, Attribute's value or Content?)
 *    2nd (.*) captures the attribute causing the Error (e.g. is it 'id', 'href', 'src' attribute etc)
 *    3rd (.*) captures type of error's expected value (e.g. 'Arial')
 *    4th (.*) captures type of error's actual value (e.g. "Arial")
 *  Example lines -
 *    1. 'Attribute 'font-family': expected value ''Arial'' instead of '"Arial"''
 *    2. 'Attribute 'id': expected value 'raphael-marker-endblock33-obj5c60j' instead of 'raphael-marker-endblock33-obj5ihv5''
 *
 * 2. Attribute is missing, usually caused by attributes that have the deprecated 'xlink:' resulting in the expected
 * output to differ from the actual output.
 *  regex2 case:
 *    /(.*) '(.*)' (is missed)/g;
 *    1st (.*) captures the category causing the Error (e.g. is it Tag, Attribute, Attribute's value or Content?)
 *    2nd (.*) captures the class causing the Error (e.g. is it 'id', 'href', 'src' class etc)
 *  Example line -
 *    1. 'Attribute 'xlink:href' is missed'
 *
 * 3. Extra attribute case, in most cases are caused by the above ignore case 2.
 *  regex3 case:
 *    /Extra attribute '(.*)'/g;
 *    (.*) captures the class causing the Error (e.g. is it 'id', 'href', 'src' class etc)
 *  Example line -
 *    1. 'Extra attribute 'href''
 *
 * 4. Different text content, sometimes as a result of different container attribute values (e.g. different width/height etc)
 * from expected output
 *  regex4 case:
 *    /Expected text '(.*)' instead of '(.*)'/g;
 *    1st (.*) captures the category causing the Error (e.g. is it Tag, Attribute, Attribute's value or Content?)
 *    2nd (.*) captures the class causing the Error (e.g. is it 'id', 'href', 'src' class etc)
 *  Example line -
 *    1. 'Expected text 'long long time,' instead of 'long long'
 *    2. 'Expected text 'so long that' instead of 'time, so long'
 *
 * @param {Array} diff - Array containing differences found by domCompare library
 * @param {String} exp - HTML string of expected output
 * @param {Element} act - HTML Element containing rendered (actual) output
 * @return {Array} returns an Array of non-formatting related errors (e.g. critical errors)
 **/
function regexHelper(diff, exp, act) {
  const ignoredAttr = ['id', 'marker-end', 'x1', 'x2', 'x', 'y1', 'y2', 'y', 'width',
    'height', 'd', 'dy', 'r', 'style', 'viewBox', 'transform', 'points'];
  const missedArray = [];

  const errorArray = [];
  diff.forEach((line) => {
    const regex1 = /(.*) '(.*)':.* '(.*)'.*'(.*)'/g;
    const regex2 = /(.*) '(.*)' (is missed)/g;
    const regex3 = /Extra (attribute|element) '(.*)'/g;
    const regex4 = /Expected text '(.*)' instead of '(.*)'/g;
    const arr1 = regex1.exec(line.message);
    const arr2 = regex2.exec(line.message);
    const arr3 = regex3.exec(line.message);
    const arr4 = regex4.exec(line.message);
    if (arr1 !== null) {
      // ignore expected value error
      if (arr1[1] === 'Attribute') {
        if (arr1[2] === 'font-family') {
          const expected = arr1[3].replace(/'/g, '');
          const actual = arr1[4].replace(/"/g, '');
          if (expected !== actual) {
            errorArray.push(line);
          }
        // certain cases of id can be ignored
        } else if (arr1[2] === 'id') {
          // Case 1
          if (arr1[3].indexOf('raphael-marker-endblock33-obj') !== -1
              && arr1[4].indexOf('raphael-marker-endblock33-obj') !== -1) {
                // Ignore, not an actual error. Just randomly generated id value
          } else if ((arr1[3].indexOf('arrowhead') !== -1
              && arr1[4].indexOf('arrowhead') !== -1)) {
                // Ignore, not an actual error.
          } else if ((arr1[3].indexOf('actor') !== -1
              && arr1[4].indexOf('actor') !== -1)) {
                // Ignore, not an actual error.
          } else {
            errorArray.push(line);
          }
        } else if (ignoredAttr.indexOf(arr1[2]) === -1) {
          if (arr1[3] !== arr1[4]) {
            errorArray.push(line);
          }
        }
      }
    } else if (arr2 !== null) {
      // ignore missing attribute error
      if (arr2[2].match(/xlink:/g) !== null) {
        missedArray.push(arr2[2].replace(/xlink:/g, ''));
      } else if (arr2[2].match(/:xlink/g) !== null) {
        missedArray.push(arr2[2].replace(/:xlink/g, ''));
      } else if (arr2[2].match(/tspan/g) !== null) {
        // Missing tspan could be due to different word wrapping, but shouldn't ignore
        // in case of true error

        // Check the node where the error is thrown and concantate the strings!
        // Capture the path of node where the error is thrown
        const regexNode = /(.*)/g;
        const arrNode = regexNode.exec(line.node);
        // Check if error is due to word wrapping
        const key = arrNode[1];
        if (!textExpected.has(key) && !textActual.has(key)) {
          // If don't have, means error is not due to word wrapping
          // hence should report as error
          errorArray.push(line);
        } else if (textExpected.has(key) && textActual.has(key)) {
          // If have in textExpected and textActual,
          // Check if strings match, in case text is different
          if (textExpected.get(key) !== textActual.get(key)) {
            errorArray.push(line);
          }
        }
      } else {
        errorArray.push(line);
      }
    } else if (arr3 !== null) {
      // ignore extra attribute/element error
      if (missedArray.length !== 0) {
        const pos = missedArray.indexOf(arr3[2]);
        if (pos !== -1) {
          // Ignore, not an actual error
          missedArray.splice(pos, 1);
        } else if (arr3[1] !== 'element') {
          errorArray.push(line);
        }
      }
    } else if (arr4 !== null) {
      // Check the node where the error is thrown and concantate the strings!
      // Capture the path of node where the error is thrown
      const regexNode = /(.*)\//g;
      const arrNode = regexNode.exec(line.node);
      // Use the path as a key to check for matching content
      // and store the strings in textExpected & textActual
      const key = arrNode[1];

      // Check if textExpected & textActual already contains the respective key
      if (!textExpected.has(key) && !textActual.has(key)) {
        // If don't have, use helper function to find the contents in the respective
        // and compare
        if (!helper(key, exp, act)) {
          errorArray.push(line);
        }
      } else if (textExpected.has(key) && textActual.has(key)) {
        // If have, check if contents match in textExpected and textActual
        if (textExpected.get(key) !== textActual.get(key)) {
          errorArray.push(line);
        }
      }
    } else {
      // add into error array
      errorArray.push(line);
    }
  });
  return errorArray;
}

/**
 * Compare expected HTML output and actual HTML output, and return an array of
 * all the differences found.
 * @param {String} output - Expected output HTML string
 * @param {Element} input - Actual rendered HTML element containing the rendered string
 * @return {Array} Array containing all the differences found by dom-compare library
 */
function compareDom(output, input) {
  // Remove carriage return from string read from a .txt file
  const outputContent = output.replace(/(\r\n)/g, '\n');
  // Prepare the output as an HTML Element for comparison using dom-compare library
  const eleOutput = iframeDoc.createElement('DIV');
  eleOutput.innerHTML = outputContent;
  // Get a comparison result object
  const results = domCompare.compare(eleOutput, input);
  return results.getDifferences();
}

describe('Renderer', () => {
  before(() => {
    document.body.appendChild(iframe);
    // Create an area for test diagrams to be rendered into for testing
    iframe.onload = () => {
      iframeDoc = iframe.contentWindow.document;
      iframeDoc.write('<div></div>');
    };
    // Initialise the above
    iframe.onload();
    diagramsRenderer = new DiagramsRenderer(iframeDoc);
  });

  describe('Diagrams Renderer', () => {
    it('should handle empty data', (done) => {
      toRender = iframeDoc.getElementsByTagName('div')[0];
      diagramsRenderer.process(toRender)
      .then((output) => {
        expect(toRender.innerHTML).to.equal('');
        done();
      })
      .catch((error) => {
        done(error);
      });
    });

    it('should handle invalid or incomplete diagram syntax', (done) => {
      // Clears Maps
      textActual.clear();
      textExpected.clear();
      // Retrieve HTML string from getHtmlData
      invalidHtmlData.then((output) => {
        toRender.innerHTML = output;
        // Render any diagrams to be drawn
        diagramsRenderer.process(toRender)
        .then((out) => {
          // Check if Expected output === Actual rendered output
          const diff = compareDom(invalidSyntaxOutput, toRender);
          const errorArray = regexHelper(diff, invalidSyntaxOutput, toRender);
          expect(errorArray.length).to.equal(0);
          done();
        })
        .catch((error) => {
          done(error);
        });
      })
      .catch((error) => {
        done(error);
      });
    // Change default timeout value because this test requires more time for rendering
    }).timeout(4000);

    it('should handle drawing of sequence diagrams', (done) => {
      // Clears Maps
      textActual.clear();
      textExpected.clear();
      // Retrieve HTML string from getHtmlData
      seqHtmlData.then((output) => {
        toRender.innerHTML = output;
        // Render any diagrams to be drawn
        diagramsRenderer.process(toRender)
        .then((out) => {
          // Check if Expected output === Actual rendered output
          const diff = compareDom(seqDiagramsOutput, toRender);
          const errorArray = regexHelper(diff, seqDiagramsOutput, toRender);
          expect(errorArray.length).to.equal(0);
          done();
        })
        .catch((error) => {
          done(error);
        });
      })
      .catch((error) => {
        done(error);
      });
    }).timeout(3000);

    it('should handle drawing of flow charts', (done) => {
      // Clears Maps
      textActual.clear();
      textExpected.clear();
      // Retrieve HTML string from getHtmlData
      flowHtmlData.then((output) => {
        toRender.innerHTML = output;
        // Render any diagrams to be drawn
        diagramsRenderer.process(toRender)
        .then((out) => {
          // Check if Expected output === Actual rendered output
          const diff = compareDom(flowChartsOutput, toRender);
          const errorArray = regexHelper(diff, flowChartsOutput, toRender);
          expect(errorArray.length).to.equal(0);
          done();
        })
        .catch((error) => {
          done(error);
        });
      })
      .catch((error) => {
        done(error);
      });
    });

    it('should handle drawing of graphviz diagrams', (done) => {
      // Clears Maps
      textActual.clear();
      textExpected.clear();
      // Retrieve HTML string from getHtmlData
      vizHtmlData.then((output) => {
        // Render any diagrams to be drawn
        toRender.innerHTML = output;
        diagramsRenderer.process(toRender)
        .then((out) => {
          // Check if Expected output === Actual rendered output
          const diff = compareDom(graphvizOutput, toRender);
          const errorArray = regexHelper(diff, graphvizOutput, toRender);
          expect(errorArray.length).to.equal(0);
          done();
        })
        .catch((error) => {
          done(error);
        });
      })
      .catch((error) => {
        done(error);
      });
    }).timeout(4000);

    it('should handle drawing of mermaid diagrams', (done) => {
      // Clears Maps
      textActual.clear();
      textExpected.clear();
      // Retrieve HTML string from getHtmlData
      mermaidHtmlData.then((output) => {
        toRender.innerHTML = output;
        // Render any diagrams to be drawn
        diagramsRenderer.process(toRender)
        .then((out) => {
          // Check if Expected output === Actual rendered output
          const diff = compareDom(mermaidOutput, toRender);
          const errorArray = regexHelper(diff, mermaidOutput, toRender);
          expect(errorArray.length).to.equal(0);
          done();
        })
        .catch((error) => {
          done(error);
        });
      })
      .catch((error) => {
        done(error);
      });
    });

    it('should handle drawing of all types of diagrams together', (done) => {
      // Clears Maps
      textActual.clear();
      textExpected.clear();
      // Retrieve HTML string from getHtmlData
      fullHtmlData.then((output) => {
        toRender.innerHTML = output;
        // Render any diagrams to be drawn
        diagramsRenderer.process(toRender)
        .then((out) => {
          // Check if Expected output === Actual rendered output
          const diff = compareDom(diagramsRenderedOutput, toRender);
          const errorArray = regexHelper(diff, diagramsRenderedOutput, toRender);
          errorArray.forEach((errorLine) => {
            /* eslint no-console: 0 */
            // console.log to be shown on travis for debugging purposes
            console.log(errorLine);
          });
          expect(errorArray.length).to.equal(0);
          done();
        })
        .catch((error) => {
          done(error);
        });
      })
      .catch((error) => {
        done(error);
      });
    // Change default timeout value because this test requires more time for rendering
    }).timeout(4000);
  });
});