core/templates/components/graph-services/graph-layout.service.spec.ts

Summary

Maintainability
F
2 wks
Test Coverage
// Copyright 2021 The Oppia Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the 'License');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS-IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Unit test for StateGraphLayoutService.
 */

import {TestBed} from '@angular/core/testing';
import {AppConstants} from 'app.constants';

import {GraphLink, GraphNodes} from 'services/compute-graph.service';
import {StateGraphLayoutService} from './graph-layout.service';

describe('Graph Layout Service', () => {
  let sgls: StateGraphLayoutService;

  // Represents the nodes of a graph, with node labels as keys, and the
  // following structure:
  //
  //   ┌────────┬───────────────State1───────────────┬───────┐
  //   │        │               │ │ │                │       │
  //   │        │        ┌──────┘ │ └──────┐         │       │
  //   ▼        ▼        ▼        ▼        ▼         ▼       ▼
  // State2   State3   State5   State6   State7   State8◄──State9
  //   │         │                                   │
  //   │         └────────────────┐                  │
  //   │                          ▼                  │
  //   └───────────────────────►State4◄──────────────┘
  //
  //                           Orphaned.
  // The corresponding value of labels are objects with the following keys
  // (only which are used in this spec file):
  //   - x0: the x-position of the top-left corner of the node, measured
  //       as a fraction of the total width.
  //   - y0: the y-position of the top-left corner of the node, measured
  //       as a fraction of the total height.
  //   - width: the width of the node, measured as a fraction of the total
  //       width.
  //   - height: the height of the node, measured as a fraction of the total
  //       height.
  //   - xLabel: the x-position of the middle of the box containing
  //       the node label, measured as a fraction of the total width.
  //       The node label is centered horizontally within this box.
  //   - yLabel: the y-position of the middle of the box containing
  //       the node label, measured as a fraction of the total height.
  //       The node label is centered vertically within this box.
  let nodeData1 = {
    State1: {
      depth: 0,
      offset: 0,
      reachable: true,
      x0: 0.07250000000000001,
      y0: 0.12666666666666668,
      xLabel: 0.17250000000000001,
      yLabel: 0.16666666666666669,
      id: 'State1',
      label: 'State1',
      height: 0.08,
      width: 0.2,
      reachableFromEnd: true,
      style: 'string',
      secondaryLabel: 'string',
      nodeClass: 'string',
      canDelete: false,
    },
    State2: {
      depth: 1,
      offset: 1.5,
      reachable: true,
      x0: 0.41000000000000003,
      y0: 0.26,
      xLabel: 0.51,
      yLabel: 0.30000000000000004,
      id: 'State2',
      label: 'State2',
      height: 0.08,
      width: 0.2,
      reachableFromEnd: true,
      style: 'string',
      secondaryLabel: 'string',
      nodeClass: 'string',
      canDelete: false,
    },
    State3: {
      depth: 1,
      offset: 2.5,
      reachable: true,
      x0: 0.6350000000000001,
      y0: 0.26,
      xLabel: 0.7350000000000001,
      yLabel: 0.30000000000000004,
      id: 'State3',
      label: 'State3',
      height: 0.08,
      width: 0.2,
      reachableFromEnd: true,
      style: 'string',
      secondaryLabel: 'string',
      nodeClass: 'string',
      canDelete: false,
    },
    State4: {
      depth: 4,
      offset: 0,
      reachable: true,
      x0: 0.07250000000000001,
      y0: 0.66,
      xLabel: 0.17250000000000001,
      yLabel: 0.7,
      id: 'State4',
      label: 'State4',
      height: 0.08,
      width: 0.2,
      reachableFromEnd: true,
      style: 'string',
      secondaryLabel: 'string',
      nodeClass: 'string',
      canDelete: false,
    },
    State5: {
      depth: 1,
      offset: 3.5,
      reachable: true,
      x0: 0.8600000000000001,
      y0: 0.26,
      xLabel: 0.9600000000000001,
      yLabel: 0.30000000000000004,
      id: 'State5',
      label: 'State5',
      height: 0.08,
      width: 0.2,
      reachableFromEnd: false,
      style: 'string',
      secondaryLabel: 'string',
      nodeClass: 'string',
      canDelete: false,
    },
    State6: {
      depth: 2,
      offset: 1.5,
      reachable: true,
      x0: 0.41000000000000003,
      y0: 0.3933333333333333,
      xLabel: 0.51,
      yLabel: 0.43333333333333335,
      id: 'State6',
      label: 'State6',
      height: 0.08,
      width: 0.2,
      reachableFromEnd: false,
      style: 'string',
      secondaryLabel: 'string',
      nodeClass: 'string',
      canDelete: false,
    },
    State7: {
      depth: 2,
      offset: 2.5,
      reachable: true,
      x0: 0.6350000000000001,
      y0: 0.3933333333333333,
      xLabel: 0.7350000000000001,
      yLabel: 0.43333333333333335,
      id: 'State7',
      label: 'State7',
      height: 0.08,
      width: 0.2,
      reachableFromEnd: false,
      style: 'string',
      secondaryLabel: 'string',
      nodeClass: 'string',
      canDelete: false,
    },
    State8: {
      depth: 3,
      offset: 0,
      reachable: true,
      x0: 0.07250000000000001,
      y0: 0.5266666666666667,
      xLabel: 0.17250000000000001,
      yLabel: 0.5666666666666667,
      id: 'State8',
      label: 'State8',
      height: 0.08,
      width: 0.2,
      reachableFromEnd: true,
      style: 'string',
      secondaryLabel: 'string',
      nodeClass: 'string',
      canDelete: false,
    },
    State9: {
      depth: 1,
      offset: 0.5,
      reachable: true,
      x0: 0.185,
      y0: 0.26,
      xLabel: 0.28500000000000003,
      yLabel: 0.30000000000000004,
      id: 'State9',
      label: 'State9',
      height: 0.08,
      width: 0.2,
      reachableFromEnd: true,
      style: 'string',
      secondaryLabel: 'string',
      nodeClass: 'string',
      canDelete: false,
    },
    Orphaned: {
      depth: 5,
      offset: 0,
      reachable: false,
      x0: 0.07250000000000001,
      y0: 0.7933333333333333,
      xLabel: 0.17250000000000001,
      yLabel: 0.8333333333333333,
      id: 'Orphaned',
      label: 'Orphaned',
      height: 0.08,
      width: 0.2,
      reachableFromEnd: false,
      style: 'string',
      secondaryLabel: 'string',
      nodeClass: 'string',
      canDelete: false,
    },
  };
  let links1: GraphLink[] = [
    {
      source: 'State1',
      target: 'State1',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State1',
      target: 'State2',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State1',
      target: 'State3',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State1',
      target: 'State5',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State1',
      target: 'State6',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State1',
      target: 'State7',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State1',
      target: 'State8',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State1',
      target: 'State9',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State2',
      target: 'State4',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State3',
      target: 'State4',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State9',
      target: 'State8',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State8',
      target: 'State4',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
  ];

  let links2: GraphLink[] = [
    {
      source: 'State1',
      target: 'State1',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State1',
      target: 'State2',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State1',
      target: 'State3',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State2',
      target: 'State4',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
    {
      source: 'State3',
      target: 'State4',
      linkProperty: '',
      connectsDestIfStuck: false,
    },
  ];

  beforeEach(() => {
    sgls = TestBed.inject(StateGraphLayoutService);
  });

  it('should create adjacency lists', () => {
    let nodes: GraphNodes = {
      State1: 'State1',
      State2: 'State2',
      State3: 'State3',
      State4: 'State4',
    };
    let expectedAdjacencyLists = {
      State1: ['State2', 'State3'],
      State2: ['State4'],
      State3: ['State4'],
      State4: [],
    };

    expect(sgls.getGraphAsAdjacencyLists(nodes, links2)).toEqual(
      expectedAdjacencyLists
    );
  });

  it('should return indentation levels for a segment of nodes', () => {
    let adjacencyLists = {
      State1: ['State2', 'State3', 'State4'],
      State2: ['State3', 'State4'],
      State3: ['State4', 'State5'],
      State4: ['State5'],
      State5: [],
    };

    let longestPathIds: string[] = [
      'State1',
      'State2',
      'State3',
      'State4',
      'State5',
    ];

    expect(sgls.getIndentationLevels(adjacencyLists, longestPathIds)).toEqual([
      0, 0.5, 1, 0, 0,
    ]);

    let shortestPathIds: string[] = ['State1', 'State4', 'State5'];
    expect(sgls.getIndentationLevels(adjacencyLists, shortestPathIds)).toEqual([
      0, 0, 0,
    ]);
  });

  it(
    'should not return indentation level greater' +
      ' than MAX_INDENTATION_LEVEL',
    () => {
      // ┌───────────┐
      // │   State1  ├──┐
      // └────┬──────┘  │
      //      │         ▼
      //      │ ┌───────────┐
      //      │ │   State2  ├──┐
      //      │ └┬──────────┘  │
      //      │  │             ▼
      //      │  │     ┌───────────┐
      //      │  │ ┌───┤   State6  ├──┐
      //      │  │ │   └───────────┘  │
      //      │  │ │                  ▼
      //      │  │ │          ┌───────────┐
      //      │  │ │ ┌────────┤   State8  ├──┐
      //      │  │ │ │        └───────────┘  │
      //      │  │ │ │                       ▼
      //      │  │ │ │               ┌───────────┐
      //      │  │ │ │ ┌─────────────┤   State7  ├──┐
      //      │  │ │ │ │             └───────────┘  │
      //      │  │ │ │ │                            ▼
      //      │  │ │ │ │                    ┌───────────┐
      //      │  │ │ │ │                 ┌──┤   State9  │
      //      │  │ │ │ │                 │  └─────┬─────┘
      //      │  │ │ │ │                 │        │
      //      │  │ │ │ │                 │  ┌─────▼─────┐
      //      │  │ │ │ │                 │  │   State10 │
      //      ▼  ▼ ▼ ▼ ▼                 │  └─┬─────────┘
      // ┌───────────┐ ◄─────────────────┘    │
      // │   State3  │                        │
      // └─────┬─────┘◄───────────────────────┘
      //       │
      // ┌─────▼─────┐
      // │   State5  │
      // └───────────┘
      // Here, State1, State2, State6, State8, State7, State9 have indentation
      // level equal to 0, 0.5, 1, 1.5, 2, 2.5. But, State10 does not have
      // indentation level equal to 3, as it is placed right below State9.
      // So, the indentation level of State10 is also 2.5.
      let adjacencyLists = {
        State1: ['State2', 'State3'],
        State2: ['State3', 'State6'],
        State6: ['State3', 'State8'],
        State8: ['State3', 'State7'],
        State7: ['State3', 'State9'],
        State9: ['State3', 'State10'],
        State10: ['State3'],
        State3: ['State5'],
        State5: [],
      };

      let trunkNodeIds: string[] = [
        'State1',
        'State2',
        'State6',
        'State8',
        'State7',
        'State9',
        'State10',
        'State3',
        'State5',
      ];

      let returnedIndentationLevels = sgls.getIndentationLevels(
        adjacencyLists,
        trunkNodeIds
      );
      returnedIndentationLevels.forEach(indentationLevel => {
        expect(indentationLevel).toBeLessThanOrEqual(
          sgls.MAX_INDENTATION_LEVEL
        );
      });
    }
  );

  it('should return augmented links with bezier curves', () => {
    let nodeData = {
      State1: {
        depth: 0,
        offset: 0,
        reachable: true,
        x0: 0.07250000000000001,
        y0: 0.15333333333333335,
        xLabel: 0.1625,
        yLabel: 0.23333333333333334,
        id: 'State1',
        label: 'State1',
        height: 0.16,
        width: 0.18000000000000002,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State2: {
        depth: 1,
        offset: 0,
        reachable: true,
        x0: 0.07250000000000001,
        y0: 0.42000000000000004,
        xLabel: 0.1625,
        yLabel: 0.5,
        id: 'State2',
        label: 'State2',
        height: 0.16,
        width: 0.18000000000000002,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State3: {
        depth: 1,
        offset: 1,
        reachable: true,
        x0: 0.29750000000000004,
        y0: 0.42000000000000004,
        xLabel: 0.3875,
        yLabel: 0.5,
        id: 'State3',
        label: 'State3',
        height: 0.16,
        width: 0.18000000000000002,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State4: {
        depth: 2,
        offset: 0,
        reachable: true,
        x0: 0.07250000000000001,
        y0: 0.6866666666666666,
        xLabel: 0.1625,
        yLabel: 0.7666666666666666,
        id: 'State4',
        label: 'State4',
        height: 0.16,
        width: 0.18000000000000002,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
    };

    // Here, bezier curve follows the format 'M%f %f Q %f %f %f %f'. The
    // floating point values are calculated with the help of the position values
    // of source and target nodes (in links2) and nodeData.
    let expectedBezierCurveValues = [
      'M0.1625 0.31333333333333335 Q 0.2025 0.3666666666666667 0.1625 0.42',
      'M0.23 0.31333333333333335 Q 0.30557165934031566' +
        ' 0.3408718290982754 0.32 0.42',
      'M0.1625 0.5800000000000001 Q 0.2025 0.6333333333333333' +
        ' 0.1625 0.6866666666666665',
      'M0.32 0.5800000000000001 Q 0.30557165934031566' +
        ' 0.6591281709017246 0.23000000000000004 0.6866666666666665',
    ];

    let returnedAugmentedLinks = sgls.getAugmentedLinks(nodeData, links2);
    let returnedBezierCurveValues = [];

    // Starting with index 1 as, links2 has first link with same source and
    // target node. So, first augmentedLink will not have a Bezier curve.
    if (returnedAugmentedLinks) {
      for (var i = 1; i < returnedAugmentedLinks.length; i++) {
        returnedBezierCurveValues.push(returnedAugmentedLinks[i].d);
      }
    }

    // Check if the returned augmented links have a bezier curve
    // which is equal to the expected value.
    expect(returnedBezierCurveValues).toEqual(expectedBezierCurveValues);
  });

  it(
    'should return undefined when source and target nodes overlap' +
      ' while processing augmented links',
    () => {
      // The nodes State1 and State2 overlap as State1.xLabel === State2.xLabel
      // and State1.yLabel === State2.yLabel .
      let nodeData = {
        State1: {
          depth: 0,
          offset: 0,
          reachable: true,
          x0: 0.07250000000000001,
          y0: 0.15333333333333335,
          xLabel: 0.1625,
          yLabel: 0.23333333333333334,
          id: 'State1',
          label: 'State1',
          height: 0.16,
          width: 0.18000000000000002,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State2: {
          depth: 1,
          offset: 0,
          reachable: true,
          x0: 0.07250000000000001,
          y0: 0.42000000000000004,
          xLabel: 0.1625,
          yLabel: 0.23333333333333334,
          id: 'State2',
          label: 'State2',
          height: 0.16,
          width: 0.18000000000000002,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
      };

      let links = [
        {
          source: 'State1',
          target: 'State2',
          linkProperty: '',
          connectsDestIfStuck: false,
        },
      ];

      expect(sgls.getAugmentedLinks(nodeData, links)).toEqual([]);
    }
  );

  it('should get correct graph width and height', () => {
    let nodeData = {
      State1: {
        depth: 1,
        offset: 0,
        reachable: true,
        x0: 0.07250000000000001,
        y0: 0.42000000000000004,
        xLabel: 0.1625,
        yLabel: 0.5,
        id: 'State1',
        label: 'State1',
        height: 0.16,
        width: 0.18000000000000002,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      Introduction: {
        depth: 0,
        offset: 0,
        reachable: true,
        x0: 0.07250000000000001,
        y0: 0.15333333333333335,
        xLabel: 0.1625,
        yLabel: 0.23333333333333334,
        id: 'Introduction',
        label: 'Introduction',
        height: 0.16,
        width: 0.18000000000000002,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      End: {
        depth: 2,
        offset: 0,
        reachable: true,
        x0: 0.07250000000000001,
        y0: 0.6866666666666666,
        xLabel: 0.1625,
        yLabel: 0.7666666666666666,
        id: 'End',
        label: 'End',
        height: 0.16,
        width: 0.18000000000000002,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State2: {
        depth: 1,
        offset: 1,
        reachable: true,
        x0: 0.29750000000000004,
        y0: 0.42000000000000004,
        xLabel: 0.3875,
        yLabel: 0.5,
        id: 'State2',
        label: 'State2',
        height: 0.16,
        width: 0.18000000000000002,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
    };

    let graphWidthUpperBoundInPixels = sgls.getGraphWidth(
      AppConstants.MAX_NODES_PER_ROW,
      AppConstants.MAX_NODE_LABEL_LENGTH
    );
    let graphHeightInPixels = sgls.getGraphHeight(nodeData);

    // 10.5 is a rough upper bound for the width of a single letter in pixels,
    // used as a scaling factor to determine width of graph nodes.
    expect(graphWidthUpperBoundInPixels).toBe(
      AppConstants.MAX_NODES_PER_ROW * AppConstants.MAX_NODE_LABEL_LENGTH * 10.5
    );
    // Here, graphHeightInPixels = 70 * (maxDepth + 1), here maxDepth is 2.
    expect(graphHeightInPixels).toBe(210);
  });

  it(
    'should get graph width and height when nodes' + ' overflow to next row',
    () => {
      let graphWidthInPixels = sgls.getGraphWidth(
        AppConstants.MAX_NODES_PER_ROW,
        AppConstants.MAX_NODE_LABEL_LENGTH
      );
      let graphHeightInPixels = sgls.getGraphHeight(nodeData1);

      // 10.5 is a rough upper bound for the width of a single letter in pixels,
      // used as a scaling factor to determine width of graph nodes.
      expect(graphWidthInPixels).toBe(
        AppConstants.MAX_NODES_PER_ROW *
          AppConstants.MAX_NODE_LABEL_LENGTH *
          10.5
      );

      // Here, graphHeightInPixels = 70 * (maxDepth + 1), here maxDepth is 5.
      expect(graphHeightInPixels).toBe(420);
    }
  );

  it('should compute graph layout', () => {
    spyOn(sgls, 'computeLayout').and.returnValue(nodeData1);

    let nodes: GraphNodes = {
      State1: 'State1',
      State2: 'State2',
      State3: 'State3',
      State4: 'State4',
      State5: 'State5',
      State6: 'State6',
      State7: 'State7',
      State8: 'State8',
      State9: 'State9',
      Orphaned: 'Orphaned',
    };

    let initNodeId: string = 'State1';
    let finalNodeIds: string[] = ['State4'];

    expect(sgls.computeLayout(nodes, links1, initNodeId, finalNodeIds)).toEqual(
      nodeData1
    );
  });

  it(
    'should overflow nodes to next row if there are' +
      ' too many nodes at a depth',
    () => {
      let MAX_NODES_PER_ROW = AppConstants.MAX_NODES_PER_ROW;
      let nodes: GraphNodes = {
        State0: 'State0',
        End: 'End',
      };

      let initNodeId: string = 'State0';
      let finalNodeIds: string[] = ['End'];
      let links = [];

      for (let i = 1; i <= MAX_NODES_PER_ROW + 1; i++) {
        let stateName = 'State' + (i + 1);
        nodes[stateName] = stateName;

        links.push({
          source: 'State0',
          target: stateName,
          linkProperty: '',
          connectsDestIfStuck: false,
        });
        links.push({
          source: stateName,
          target: 'End',
          linkProperty: '',
          connectsDestIfStuck: false,
        });
      }

      let returnedLayoutNodeData = sgls.computeLayout(
        nodes,
        links,
        initNodeId,
        finalNodeIds
      );
      let countNodesDepthOne: number = 0;
      for (let nodeId in nodes) {
        if (returnedLayoutNodeData[nodeId].depth === 1) {
          countNodesDepthOne++;
        }
      }

      expect(countNodesDepthOne).toEqual(MAX_NODES_PER_ROW);
    }
  );

  it('should place orhpaned node at max depth while computing layout', () => {
    let nodes = {
      End: 'End',
      State0: 'State0',
      Orphan: 'Orphan',
      State1: 'State1',
      State2: 'State2',
      State3: 'State3',
      State4: 'State4',
      State5: 'State5',
    };

    let links = [
      {
        source: 'State5',
        target: 'End',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
      {
        source: 'State4',
        target: 'End',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
      {
        source: 'State3',
        target: 'End',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
      {
        source: 'State2',
        target: 'End',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
      {
        source: 'State1',
        target: 'End',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
      {
        source: 'State0',
        target: 'State1',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
      {
        source: 'State0',
        target: 'State2',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
      {
        source: 'State0',
        target: 'State3',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
      {
        source: 'State0',
        target: 'State4',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
      {
        source: 'State0',
        target: 'State5',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
      {
        source: 'State0',
        target: 'State0',
        linkProperty: '',
        connectsDestIfStuck: false,
      },
    ];
    let initNodeId = 'State0';
    let finalNodeIds = ['End'];

    let returnedLayout = sgls.computeLayout(
      nodes,
      links,
      initNodeId,
      finalNodeIds
    );

    expect(returnedLayout.End.depth).toBe(3);
    expect(returnedLayout.Orphan.depth).toBe(4);
  });

  it('should get last computed layout', () => {
    let nodes: GraphNodes = {
      State1: 'State1',
      State2: 'State2',
      State3: 'State3',
      State4: 'State4',
      State5: 'State5',
      State6: 'State6',
      State7: 'State7',
      State8: 'State8',
      State9: 'State9',
      Orphaned: 'Orphaned',
    };

    let initNodeId: string = 'State1';
    let finalNodeIds: string[] = ['State4'];

    expect(sgls.getLastComputedArrangement()).toBe(null);

    let computedLayout = sgls.computeLayout(
      nodes,
      links1,
      initNodeId,
      finalNodeIds
    );

    expect(sgls.getLastComputedArrangement()).toEqual(computedLayout);
  });

  it(
    'should return graph boundaries with width less than equal to' +
      ' maximum allowed graph width',
    () => {
      // Here, nodeDataWithPositionValueInPixels1, 2 and 3 have position values
      // (x0, xLabel, width etc.) in terms of pixels.
      // nodeDataWithPositionInPixels1, 2 and 3 are node data of graphs with
      // MAX_NODE_PER_ROW - 1, MAX_NODE_PER_ROW and MAX_NODE_PER_ROW + 1 nodes in
      // a row. Right now, MAX_NODE_PER_ROW is 4.

      // ┌──────────────┐
      // │ Introduction │
      // └──┬────────┬──┴──────┐
      //    │        │         │
      // ┌──▼───┐ ┌──▼───┐ ┌───▼──┐
      // │State1│ │State2│ │State3│
      // └──────┘ └──────┘ └──────┘.
      let nodeDataWithPositionValueInPixels1 = {
        State1: {
          depth: 1,
          offset: 0,
          reachable: true,
          x0: 45.675000000000004,
          y0: 81.19999999999999,
          xLabel: 102.375,
          yLabel: 98.00000000000001,
          id: 'State1',
          label: 'State1',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State2: {
          depth: 1,
          offset: 1,
          reachable: true,
          x0: 187.42500000000004,
          y0: 81.19999999999999,
          xLabel: 244.125,
          yLabel: 98.00000000000001,
          id: 'State2',
          label: 'State2',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State3: {
          depth: 1,
          offset: 2,
          reachable: true,
          x0: 329.17500000000007,
          y0: 81.19999999999999,
          xLabel: 385.875,
          yLabel: 98.00000000000001,
          id: 'State3',
          label: 'State3',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        Introduction: {
          depth: 0,
          offset: 0,
          reachable: true,
          x0: 45.675000000000004,
          y0: 25.200000000000003,
          xLabel: 102.375,
          yLabel: 42.00000000000001,
          id: 'Introduction',
          label: 'Introduction',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
      };

      // ┌──────────────┬───────────────┐
      // │ Introduction │               │
      // └──┬────────┬──┴──────┐        │
      //    │        │         │        │
      // ┌──▼───┐ ┌──▼───┐ ┌───▼──┐  ┌──▼───┐
      // │State1│ │State2│ │State3│  │State4│
      // └──────┘ └──────┘ └──────┘  └──────┘.
      let nodeDataWithPositionValueInPixels2 = {
        State1: {
          depth: 1,
          offset: 0,
          reachable: true,
          x0: 45.675000000000004,
          y0: 81.19999999999999,
          xLabel: 102.375,
          yLabel: 98.00000000000001,
          id: 'State1',
          label: 'State1',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State2: {
          depth: 1,
          offset: 1,
          reachable: true,
          x0: 187.42500000000004,
          y0: 81.19999999999999,
          xLabel: 244.125,
          yLabel: 98.00000000000001,
          id: 'State2',
          label: 'State2',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State3: {
          depth: 1,
          offset: 2,
          reachable: true,
          x0: 329.17500000000007,
          y0: 81.19999999999999,
          xLabel: 385.875,
          yLabel: 98.00000000000001,
          id: 'State3',
          label: 'State3',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        Introduction: {
          depth: 0,
          offset: 0,
          reachable: true,
          x0: 45.675000000000004,
          y0: 25.200000000000003,
          xLabel: 102.375,
          yLabel: 42.00000000000001,
          id: 'Introduction',
          label: 'Introduction',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State4: {
          depth: 1,
          offset: 3,
          reachable: true,
          x0: 470.925,
          y0: 81.19999999999999,
          xLabel: 527.625,
          yLabel: 98.00000000000001,
          id: 'State4',
          label: 'State4',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
      };

      //      ┌──────────────┬───────────────┐
      // ┌────┤ Introduction │               │
      // │    └──┬────────┬──┴──────┐        │
      // │       │        │         │        │
      // │    ┌──▼───┐ ┌──▼───┐ ┌───▼──┐  ┌──▼───┐
      // │    │State1│ │State2│ │State3│  │State4│
      // │    └──────┘ └──────┘ └──────┘  └──────┘
      // │             ┌──────┐
      // └─────────────►State5│
      //               └──────┘
      // So, here State5 moves on to the next row.
      let nodeDataWithPositionValueInPixels3 = {
        State1: {
          depth: 1,
          offset: 0,
          reachable: true,
          x0: 45.675000000000004,
          y0: 88.2,
          xLabel: 102.375,
          yLabel: 105,
          id: 'State1',
          label: 'State1',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State2: {
          depth: 1,
          offset: 1,
          reachable: true,
          x0: 187.42500000000004,
          y0: 88.2,
          xLabel: 244.125,
          yLabel: 105,
          id: 'State2',
          label: 'State2',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State3: {
          depth: 1,
          offset: 2,
          reachable: true,
          x0: 329.17500000000007,
          y0: 88.2,
          xLabel: 385.875,
          yLabel: 105,
          id: 'State3',
          label: 'State3',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        Introduction: {
          depth: 0,
          offset: 0,
          reachable: true,
          x0: 45.675000000000004,
          y0: 32.2,
          xLabel: 102.375,
          yLabel: 49,
          id: 'Introduction',
          label: 'Introduction',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State4: {
          depth: 1,
          offset: 3,
          reachable: true,
          x0: 470.925,
          y0: 88.2,
          xLabel: 527.625,
          yLabel: 105,
          id: 'State4',
          label: 'State4',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State5: {
          depth: 2,
          offset: 1,
          reachable: true,
          x0: 187.42500000000004,
          y0: 144.2,
          xLabel: 244.125,
          yLabel: 161,
          id: 'State5',
          label: 'State5',
          height: 33.6,
          width: 126,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
      };

      let actualGraphBoundariesInPixels1 = sgls.getGraphBoundaries(
        nodeDataWithPositionValueInPixels1
      );
      let actualGraphBoundariesInPixels2 = sgls.getGraphBoundaries(
        nodeDataWithPositionValueInPixels2
      );
      let actualGraphBoundariesInPixels3 = sgls.getGraphBoundaries(
        nodeDataWithPositionValueInPixels3
      );

      // The width of graph is calculated as difference between left and right
      // edge.
      let actualWidthInPixels1 =
        actualGraphBoundariesInPixels1.right -
        actualGraphBoundariesInPixels1.left;
      let actualWidthInPixels2 =
        actualGraphBoundariesInPixels2.right -
        actualGraphBoundariesInPixels2.left;
      let actualWidthInPixels3 =
        actualGraphBoundariesInPixels3.right -
        actualGraphBoundariesInPixels3.left;

      // This is the maximum upper bound for graph width taking padding fraction
      // into consideration. First we calculate the x0 of the rightmost node.
      // rightMostNode.x0 = HORIZONTAL_EDGE_PADDING_FRACTION +
      // fractionalGridWidth * offsetInGridRectangle, where
      // HORIZONTAL_EDGE_PADDING_FRACTION = 0.05,
      // fractionalGridWidth = (1.0 - HORIZONTAL_EDGE_PADDING_FRACTION * 2) /
      // totalColumns  = (1.0 - 0.05 * 2)/4 = 0.225, and
      // offsetInGridRectangle = rightMostNode.offset +
      // GRID_NODE_X_PADDING_FRACTION = 3 + 0.1 = 3.1
      // So, rightMostNode.x0 = 0.05 + 0.225 * 3.1 = 0.7475. Converting it to
      // pixel, rightMostNode.x0 = 630 * 0.747 = 470.925. Now, the rightEdge =
      // rightMostNode.x0 + BORDER_PADDING + rightMostNode.width = 470.925 + 5 +
      // 126 = 601.925.
      // Now, if we calculate the leftEdge of leftMostState (here State1), we'll
      // get 40.675000000000004. So width = 601.925 - 40.675000000000004 = 561.25.
      let widthUpperBoundInPixels = 561.25;

      // The height of graph calculated as difference b/w bottom and right top.
      let actualHeightInPixels1 =
        actualGraphBoundariesInPixels1.bottom -
        actualGraphBoundariesInPixels1.top;
      let actualHeightInPixels2 =
        actualGraphBoundariesInPixels2.bottom -
        actualGraphBoundariesInPixels2.top;
      let actualHeightInPixels3 =
        actualGraphBoundariesInPixels3.bottom -
        actualGraphBoundariesInPixels3.top;

      // Here, we see that for 3 nodes the graph width is less than
      // the upper bound, for  4 nodes the graph width is equal to the
      // upper bound and the width becomes constant for nodes > 4.
      expect(actualWidthInPixels1).toBeLessThan(actualWidthInPixels2);
      expect(actualWidthInPixels2).toEqual(widthUpperBoundInPixels);
      expect(actualWidthInPixels3).toEqual(widthUpperBoundInPixels);

      // As height does not have an upper bound, it increases as a node overflows
      // to the next row.
      expect(actualHeightInPixels1).toEqual(actualHeightInPixels2);
      expect(actualHeightInPixels2).toBeLessThan(actualHeightInPixels3);
    }
  );

  it(
    'should return graph boundaries with height equal to' + ' the graph height',
    () => {
      let nodeData = {
        State1: {
          depth: 1,
          offset: 0,
          reachable: true,
          x0: 0.07250000000000001,
          y0: 0.42000000000000004,
          xLabel: 0.1625,
          yLabel: 0.5,
          id: 'State1',
          label: 'State1',
          height: 0.16,
          width: 0.18000000000000002,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        Introduction: {
          depth: 0,
          offset: 0,
          reachable: true,
          x0: 0.07250000000000001,
          y0: 0.15333333333333335,
          xLabel: 0.1625,
          yLabel: 0.23333333333333334,
          id: 'Introduction',
          label: 'Introduction',
          height: 0.16,
          width: 0.18000000000000002,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        End: {
          depth: 2,
          offset: 0,
          reachable: true,
          x0: 0.07250000000000001,
          y0: 0.6866666666666666,
          xLabel: 0.1625,
          yLabel: 0.7666666666666666,
          id: 'End',
          label: 'End',
          height: 0.16,
          width: 0.18000000000000002,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
        State2: {
          depth: 1,
          offset: 1,
          reachable: true,
          x0: 0.29750000000000004,
          y0: 0.42000000000000004,
          xLabel: 0.3875,
          yLabel: 0.5,
          id: 'State2',
          label: 'State2',
          height: 0.16,
          width: 0.18000000000000002,
          reachableFromEnd: false,
          style: 'style',
          secondaryLabel: 'secondaryLabel',
          nodeClass: 'nodeClass',
          canDelete: false,
        },
      };
      let graphWidthUpperBoundInPixels = sgls.getGraphWidth(
        AppConstants.MAX_NODES_PER_ROW,
        AppConstants.MAX_NODE_LABEL_LENGTH
      );
      let graphHeightInPixels = sgls.getGraphHeight(nodeData);
      let nodeDataWithPositionValueInPixels = sgls.modifyPositionValues(
        nodeData,
        graphWidthUpperBoundInPixels,
        graphHeightInPixels
      );

      // The expectedGraphBoundariesInPixels are calculated from the x0 and y0
      // values from nodeDataWithPositionValueInPixels, where leftEdge is
      // minimum(nodeDataWithPositionValueInPixels[nodeId].x0 - BORDER_PADDING)
      // of all nodes, and rightEdge is
      // maximum(nodeDataWithPositionValueInPixels[nodeId].x0 + BORDER_PADDING +
      // nodeDataWithPositionValueInPixels[nodeId].width) of all nodes. Similarly,
      // bottomEdge and topEdge are calculated using y0 and node height.
      let expectedGraphBoundariesInPixels = {
        bottom: 182.79999999999998,
        left: 40.675000000000004,
        right: 305.82500000000005,
        top: 27.200000000000003,
      };

      let expectedHeightInPixels =
        expectedGraphBoundariesInPixels.bottom -
        expectedGraphBoundariesInPixels.top;

      expect(expectedHeightInPixels).toBeLessThanOrEqual(graphHeightInPixels);
      expect(
        sgls.getGraphBoundaries(nodeDataWithPositionValueInPixels)
      ).toEqual(expectedGraphBoundariesInPixels);
    }
  );

  it('should modify position values in node data to use pixels', () => {
    let nodeData = {
      State1: {
        depth: 0,
        offset: 0,
        reachable: true,
        x0: 0.07250000000000001,
        y0: 0.12666666666666668,
        xLabel: 0.1625,
        yLabel: 0.16666666666666669,
        id: 'State1',
        label: 'State1',
        height: 0.08,
        width: 0.2,
        reachableFromEnd: true,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State2: {
        depth: 1,
        offset: 1.5,
        reachable: true,
        x0: 0.41000000000000003,
        y0: 0.26,
        xLabel: 0.5,
        yLabel: 0.30000000000000004,
        id: 'State2',
        label: 'State2',
        height: 0.08,
        width: 0.2,
        reachableFromEnd: true,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State3: {
        depth: 1,
        offset: 2.5,
        reachable: true,
        x0: 0.6350000000000001,
        y0: 0.26,
        xLabel: 0.7250000000000001,
        yLabel: 0.30000000000000004,
        id: 'State3',
        label: 'State3',
        height: 0.08,
        width: 0.2,
        reachableFromEnd: true,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State4: {
        depth: 4,
        offset: 0,
        reachable: true,
        x0: 0.07250000000000001,
        y0: 0.66,
        xLabel: 0.1625,
        yLabel: 0.7,
        id: 'State4',
        label: 'State4',
        height: 0.08,
        width: 0.2,
        reachableFromEnd: true,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State5: {
        depth: 1,
        offset: 3.5,
        reachable: true,
        x0: 0.8600000000000001,
        y0: 0.26,
        xLabel: 0.9500000000000001,
        yLabel: 0.30000000000000004,
        id: 'State5',
        label: 'State5',
        height: 0.08,
        width: 0.2,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State6: {
        depth: 2,
        offset: 1.5,
        reachable: true,
        x0: 0.41000000000000003,
        y0: 0.3933333333333333,
        xLabel: 0.5,
        yLabel: 0.43333333333333335,
        id: 'State6',
        label: 'State6',
        height: 0.08,
        width: 0.2,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State7: {
        depth: 2,
        offset: 2.5,
        reachable: true,
        x0: 0.6350000000000001,
        y0: 0.3933333333333333,
        xLabel: 0.7250000000000001,
        yLabel: 0.43333333333333335,
        id: 'State7',
        label: 'State7',
        height: 0.08,
        width: 0.2,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State8: {
        depth: 3,
        offset: 0,
        reachable: true,
        x0: 0.07250000000000001,
        y0: 0.5266666666666667,
        xLabel: 0.1625,
        yLabel: 0.5666666666666667,
        id: 'State8',
        label: 'State8',
        height: 0.08,
        width: 0.2,
        reachableFromEnd: true,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      State9: {
        depth: 1,
        offset: 0.5,
        reachable: true,
        x0: 0.185,
        y0: 0.26,
        xLabel: 0.275,
        yLabel: 0.30000000000000004,
        id: 'State9',
        label: 'State9',
        height: 0.08,
        width: 0.2,
        reachableFromEnd: true,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
      Orphaned: {
        depth: 5,
        offset: 0,
        reachable: false,
        x0: 0.07250000000000001,
        y0: 0.7933333333333333,
        xLabel: 0.1625,
        yLabel: 0.8333333333333333,
        id: 'Orphaned',
        label: 'Orphaned',
        height: 0.08,
        width: 0.2,
        reachableFromEnd: false,
        style: 'style',
        secondaryLabel: 'secondaryLabel',
        nodeClass: 'nodeClass',
        canDelete: false,
      },
    };

    let graphWidthUpperBoundInPixels = sgls.getGraphWidth(
      AppConstants.MAX_NODES_PER_ROW,
      AppConstants.MAX_NODE_LABEL_LENGTH
    );
    let graphHeightInPixels = sgls.getGraphHeight(nodeData);

    // Here, modifiedNodeData is nodeData with position values in pixels.
    // For ex, nodeData.State1.x0 = 0.07250000000000001 which is expressed
    // in fraction of graph width. It can be converted to pixels by multiplying
    // with graph width. So, modifiedNodeDate.State1.x0 = 0.07250000000000001 *
    // graphWidthUpperBoundInPixels (630) = 47.675000000000004.
    let modifiedNodeData = sgls.modifyPositionValues(
      nodeData,
      graphWidthUpperBoundInPixels,
      graphHeightInPixels
    );

    // The expectedPositionValues are calculated similarly as given above.
    let expectedPositionValuesInPixels = {
      State1: {
        x0: 45.675000000000004,
        y0: 53.2,
        xLabel: 102.375,
        yLabel: 70.00000000000001,
        width: 126,
        height: 33.6,
      },
    };

    // Verifying the position values of State1.
    expect(modifiedNodeData.State1.x0).toEqual(
      expectedPositionValuesInPixels.State1.x0
    );
    expect(modifiedNodeData.State1.y0).toEqual(
      expectedPositionValuesInPixels.State1.y0
    );
    expect(modifiedNodeData.State1.xLabel).toEqual(
      expectedPositionValuesInPixels.State1.xLabel
    );
    expect(modifiedNodeData.State1.yLabel).toEqual(
      expectedPositionValuesInPixels.State1.yLabel
    );
    expect(modifiedNodeData.State1.width).toEqual(
      expectedPositionValuesInPixels.State1.width
    );
    expect(modifiedNodeData.State1.height).toEqual(
      expectedPositionValuesInPixels.State1.height
    );
  });
});