tests_state_state.test.js

import {describe, test, assert} from 'vitest';
import {State} from '../../src/io/state.js';

import v01Ellipse from './v0.1/state-ellipse.json?raw';
import v01LineMulti from './v0.1/state-line_multi-slice.json?raw';
import v01Line from './v0.1/state-line.json?raw';
import v01Protractor from './v0.1/state-protractor.json?raw';
import v01Rectangle from './v0.1/state-rectangle.json?raw';
import v01Roi from './v0.1/state-roi.json?raw';

import v02Arrow from './v0.2/state-arrow.json?raw';
import v02Ellipse from './v0.2/state-ellipse.json?raw';
import v02Hand from './v0.2/state-hand.json?raw';
import v02Protractor from './v0.2/state-protractor.json?raw';
import v02Rectangle from './v0.2/state-rectangle.json?raw';
import v02Roi from './v0.2/state-roi.json?raw';
import v02RulerMulti from './v0.2/state-ruler_multi-slice.json?raw';
import v02Ruler from './v0.2/state-ruler.json?raw';

import v03Arrow from './v0.3/state-arrow.json?raw';
import v03Ellipse from './v0.3/state-ellipse.json?raw';
import v03Hand from './v0.3/state-hand.json?raw';
import v03Protractor from './v0.3/state-protractor.json?raw';
import v03Rectangle from './v0.3/state-rectangle.json?raw';
import v03Roi from './v0.3/state-roi.json?raw';
import v03RulerMulti from './v0.3/state-ruler_multi-slice.json?raw';
import v03Ruler from './v0.3/state-ruler.json?raw';

import v04Arrow from './v0.4/state-arrow.json?raw';
import v04Ellipse from './v0.4/state-ellipse.json?raw';
import v04Hand from './v0.4/state-hand.json?raw';
import v04Protractor from './v0.4/state-protractor.json?raw';
import v04Rectangle from './v0.4/state-rectangle.json?raw';
import v04Roi from './v0.4/state-roi.json?raw';
import v04RulerMulti from './v0.4/state-ruler_multi-slice.json?raw';
import v04Ruler from './v0.4/state-ruler.json?raw';

import v05Arrow from './v0.5/state-arrow.json?raw';
import v05Ellipse from './v0.5/state-ellipse.json?raw';
import v05Hand from './v0.5/state-hand.json?raw';
import v05Protractor from './v0.5/state-protractor.json?raw';
import v05Rectangle from './v0.5/state-rectangle.json?raw';
import v05Roi from './v0.5/state-roi.json?raw';
import v05RulerMulti from './v0.5/state-ruler_multi-slice.json?raw';
import v05Ruler from './v0.5/state-ruler.json?raw';

/**
 * Tests for the 'app/state.js' file.
 */
/** @module tests/state */

describe('state', () => {

  /**
   * Test a state file.
   *
   * @param {object} data The test data.
   * @param {string} version The state format version.
   * @param {string} type The type of drawing.
   */
  function testState(data, version, type) {
    // read state
    const state = new State();
    const jsonData = state.fromJSON(data);
    // check drawings values
    checkDrawings(
      jsonData.drawings, jsonData.drawingsDetails, version, type);
    // delete drawing to allow simple equal check
    delete jsonData.drawings;
    delete jsonData.drawingsDetails;
    // check values expect drawings
    checkStateHeader(jsonData, version);
  }

  /**
   * Check state header.
   *
   * @param {object} jsonData The input data to check.
   * @param {string} version The state format version.
   */
  function checkStateHeader(jsonData, version) {
    // header data
    const headerData = {
      version: version,
      'window-center': 441,
      'window-width': 911,
    };
    if (parseFloat(version) <= 0.3) {
      headerData.scale = 1;
      headerData.scaleCenter = {x: 0, y: 0};
      headerData.translation = {x: 0, y: 0};
    } else {
      headerData.scale = {x: 1, y: 1};
      headerData.offset = {x: 0, y: 0};
    }
    if (parseFloat(version) <= 0.4) {
      headerData.position = [0, 0, 0];
    } else {
      headerData.position = [0, 0, 114.63];
    }
    assert.deepEqual(jsonData, headerData);
  }

  /**
   * Check drawings.
   *
   * @param {object} drawings The drawing object to check.
   * @param {object} details The drawing details.
   * @param {string} version The state format version.
   * @param {string} type The type of drawing.
   */
  function checkDrawings(drawings, details, version, type) {
    // first level: layer
    assert.equal(drawings.className, 'Layer', 'State drawings is a layer.');

    // second level: position groups
    if (drawings.children.length === 5 &&
      (type === 'ruler_multi-slice' || type === 'line_multi-slice')) {
      checkRulerDrawings(drawings.children, details, version);
    } else if (drawings.children.length === 1) {

      const layerKid = drawings.children[0];
      assert.equal(layerKid.className, 'Group',
        'Layer first level is a group.');
      assert.equal(
        layerKid.attrs.name,
        'position-group',
        'Layer first level is a position group.');
      const groupId = '#2-0';
      assert.equal(
        layerKid.attrs.id,
        groupId,
        'Position group has the proper id.');

      // third level: shape group(s)
      const posGroupKid = layerKid.children[0];
      assert.equal(
        posGroupKid.className,
        'Group',
        'Position group first level is a group.');

      const unit = parseFloat(version) <= 0.3 ? 'mm' : ' mm';

      // shape specific checks
      if (type === 'arrow') {
        checkArrowDrawing(posGroupKid, details, version);
      } else if (type === 'ruler' && version !== '0.1') {
        const refRuler = {
          id: '4gvkz8v6wzw',
          points: [51, 135, 216, 134],
          colour: '#ffff80',
          text: '165.0' + unit,
          textExpr: '{length}',
          longText: 'What a ruler!'
        };
        checkRulerDrawing(
          posGroupKid, details, version, refRuler);
      } else if (type === 'line' && version === '0.1') {
        const refLine = {
          id: '4gvkz8v6wzw',
          points: [51, 135, 216, 134],
          colour: '#ffff00',
          text: '165.0' + unit,
          textExpr: '{length}',
          longText: ''
        };
        checkRulerDrawing(
          posGroupKid, details, version, refLine);
      } else if (type === 'roi') {
        checkRoiDrawing(posGroupKid, details, version);
      } else if (type === 'hand') {
        checkHandDrawing(posGroupKid, details, version);
      } else if (type === 'ellipse') {
        checkEllipseDrawing(posGroupKid, details, version);
      } else if (type === 'protractor') {
        checkProtractorDrawing(posGroupKid, details, version);
      } else if (type === 'rectangle') {
        checkRectangleDrawing(posGroupKid, details, version);
      } else {
        assert.ok(false, 'Unknown draw type.');
      }
    } else {
      assert.ok(false, 'Not the expected number of position groups.');
    }
  }

  /**
   * Check an arrow drawing.
   *
   * @param {object} posGroupKid The position group (only) kid.
   * @param {object} details The drawing details.
   * @param {string} version The state format version.
   */
  function checkArrowDrawing(posGroupKid, details, version) {
    // check group
    assert.equal(
      posGroupKid.attrs.name, 'line-group', 'Shape group is a line group.');
    assert.ok(posGroupKid.attrs.draggable, 'Shape group must be draggable.');
    assert.equal(
      posGroupKid.attrs.id,
      'pf8zteo5r4',
      'Position group first level has the proper id.');
    assert.notEqual(
      typeof details.pf8zteo5r4,
      'undefined',
      'Details should contain data for id.');

    // kids
    assert.equal(posGroupKid.children.length, 3, 'Shape group has 3 kids.');
    let hasShape = false;
    let hasLabel = false;
    let hasPoly = false;
    for (let i = 0; i < posGroupKid.children.length; ++i) {
      const shapeGroupKid = posGroupKid.children[i];
      if (shapeGroupKid.attrs.name === 'shape') {
        hasShape = true;
        assert.equal(
          shapeGroupKid.className,
          'Line',
          'Shape group \'shape\' is a line.');
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'shape\' must not be draggable.');
        assert.deepEqual(
          shapeGroupKid.attrs.points,
          [53, 136, 139, 89],
          'Line has the proper points.');
        assert.equal(
          shapeGroupKid.attrs.stroke,
          '#ffff80',
          'Line has the proper colour.');
      } else if (shapeGroupKid.className === 'Label') {
        hasLabel = true;
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'label\' must not be draggable.');
        assert.equal(shapeGroupKid.children.length, 2, 'Label has 2 kids.');
        const labelGroupKid0 = shapeGroupKid.children[0];
        assert.equal(
          labelGroupKid0.className,
          'Text',
          'Label group first level is a text.');
        assert.equal(
          labelGroupKid0.attrs.text,
          'Eye',
          'Text has the proper value.');
        const labelGroupKid1 = shapeGroupKid.children[1];
        assert.equal(
          labelGroupKid1.className,
          'Tag',
          'Label group second level is a tag.');
      } else if (shapeGroupKid.className === 'RegularPolygon') {
        hasPoly = true;
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'extra\' must not be draggable.');
      }
    }
    assert.ok(hasShape, 'Shape group contains a shape.');
    assert.ok(hasLabel, 'Shape group contains a label.');
    assert.ok(hasPoly, 'Shape group contains a polygon.');

    // details
    const details0 = details.pf8zteo5r4;
    assert.equal(
      details0.meta.textExpr, 'Eye', 'Details textExpr has the proper value.');
    if (parseFloat(version) <= 0.3) {
      assert.equal(
        details0.meta.longText,
        'This is an eye!',
        'Details longText has the proper value.');
    }
  }

  /**
   * Check a ruler drawing.
   *
   * @param {object} posGroupKid The position group (only) kid.
   * @param {object} details The drawing details.
   * @param {string} version The state format version.
   * @param {string} ref The reference data to compare to.
   */
  function checkRulerDrawing(
    posGroupKid, details, version, ref) {
    // check group
    assert.equal(
      posGroupKid.attrs.name, 'ruler-group', 'Shape group is a ruler group.');
    assert.ok(posGroupKid.attrs.draggable, 'Shape group must be draggable.');
    assert.equal(
      posGroupKid.attrs.id,
      ref.id,
      'Position group first level has the proper id.');
    assert.notEqual(
      typeof details[ref.id],
      'undefined',
      'Details should contain data for id.');

    // kids
    assert.equal(posGroupKid.children.length, 4, 'Shape group has 4 kids.');
    let hasShape = false;
    let hasLabel = false;
    let hasTick1 = false;
    let hasTick2 = false;
    for (let i = 0; i < posGroupKid.children.length; ++i) {
      const shapeGroupKid = posGroupKid.children[i];
      if (shapeGroupKid.attrs.name === 'shape') {
        hasShape = true;
        assert.equal(
          shapeGroupKid.className,
          'Line',
          'Shape group \'shape\' is a line.');
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'shape\' must not be draggable.');
        assert.deepEqual(
          shapeGroupKid.attrs.points,
          ref.points,
          'Line has the proper points.');
        assert.equal(
          shapeGroupKid.attrs.stroke,
          ref.colour,
          'Line has the proper colour.');
      } else if (shapeGroupKid.className === 'Label') {
        hasLabel = true;
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'label\' must not be draggable.');
        assert.equal(shapeGroupKid.children.length, 2, 'Label has 2 kids.');
        const labelGroupKid0 = shapeGroupKid.children[0];
        assert.equal(
          labelGroupKid0.className,
          'Text',
          'Label group first level is a text.');
        assert.equal(
          labelGroupKid0.attrs.text,
          ref.text,
          'Text has the proper value.');
        const labelGroupKid1 = shapeGroupKid.children[1];
        assert.equal(
          labelGroupKid1.className,
          'Tag',
          'Label group second level is a tag.');
      } else if (shapeGroupKid.className === 'Line') {
        if (hasTick1) {
          hasTick2 = true;
        } else {
          hasTick1 = true;
        }
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'extra\' must not be draggable.');
      }
    }
    assert.ok(hasShape, 'Shape group contains a shape.');
    assert.ok(hasLabel, 'Shape group contains a label.');
    assert.ok(hasTick1, 'Shape group contains a tick1.');
    assert.ok(hasTick2, 'Shape group contains a tick2.');

    // details
    const details0 = details[ref.id];
    assert.equal(
      details0.meta.textExpr,
      ref.textExpr,
      'Details textExpr has the proper value.');
    if (parseFloat(version) <= 0.3) {
      assert.equal(
        details0.meta.longText,
        ref.longText,
        'Details longText has the proper value.');
    }
  }

  /**
   * Check a multi slice ruler drawing.
   *
   * @param {object} layerKids The draw layer.
   * @param {object} details The drawing details.
   * @param {string} version The state format version.
   */
  function checkRulerDrawings(layerKids, details, version) {

    const ndraws = 5;
    assert.equal(layerKids.length, ndraws, 'Layer has ' + ndraws + ' kids.');

    const unit = parseFloat(version) <= 0.3 ? 'mm' : ' mm';
    const refRulers = [
      {
        id: 'onzlkbs8p',
        points: [120, 110, 120, 60],
        colour: '#ffff00',
        text: '50.00' + unit,
        textExpr: '{length}',
        longText: (version === '0.1' ? '' : 'First ruler.')
      },
      {
        id: 'u9bvidgkjc9',
        points: [120, 110, 170, 110],
        colour: '#ff0000',
        text: '50.00' + unit,
        textExpr: '{length}',
        longText: (version === '0.1' ? '' : 'Second ruler.')
      },
      {
        id: 'c9abkegq62j',
        points: [120, 110, 120, 160],
        colour: '#ffffff',
        text: '50.00' + unit,
        textExpr: '{length}',
        longText: (version === '0.1' ? '' : 'Third ruler.')
      },
      {
        id: 'uiav43zjw1',
        points: [120, 110, 60, 110],
        colour: '#00ff00',
        text: '50.00' + unit,
        textExpr: '{length}',
        longText: (version === '0.1' ? '' : 'Fourth ruler.')
      },
      {
        id: '26ir11b9ugl',
        points: [120, 110, 120, 60],
        colour: '#ff00ff',
        text: '50.00' + unit,
        textExpr: '{length}',
        longText: (version === '0.1' ? '' : 'Fifth ruler.')
      }
    ];

    for (let i = 0; i < ndraws; ++i) {
      const layerKid = layerKids[i];
      assert.equal(
        layerKid.className,
        'Group',
        'Layer first level is a group for slice ' + i + '.');
      assert.equal(
        layerKid.attrs.name,
        'position-group',
        'Layer first level is a position group.');
      const groupId = '#2-' + (i + 1);
      assert.equal(
        layerKid.attrs.id,
        groupId,
        'Position group has the proper id.');

      // third level: shape group(s)
      const posGroupKid = layerKid.children[0];
      assert.equal(
        posGroupKid.className,
        'Group',
        'Position group first level is a group.');

      checkRulerDrawing(
        posGroupKid, details, version, refRulers[i]);
    }
  }

  /**
   * Check a roi drawing.
   *
   * @param {object} posGroupKid The position group (only) kid.
   * @param {object} details The drawing details.
   * @param {string} version The state format version.
   */
  function checkRoiDrawing(posGroupKid, details, version) {
    // check group
    assert.equal(
      posGroupKid.attrs.name,
      'roi-group',
      'Shape group is a roi group.');
    assert.ok(posGroupKid.attrs.draggable, 'Shape group must be draggable.');
    assert.equal(
      posGroupKid.attrs.id,
      '4l24ofouhmf',
      'Position group first level has the proper id.');
    assert.notEqual(
      typeof details['4l24ofouhmf'],
      'undefined',
      'Details should contain data for id.');

    // kids
    assert.equal(posGroupKid.children.length, 2, 'Shape group has 2 kids.');
    let hasShape = false;
    let hasLabel = false;
    for (let i = 0; i < posGroupKid.children.length; ++i) {
      const shapeGroupKid = posGroupKid.children[i];
      if (shapeGroupKid.attrs.name === 'shape') {
        hasShape = true;
        assert.equal(
          shapeGroupKid.className,
          'Line',
          'Shape group \'shape\' is a line.');
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'shape\' must not be draggable.');
        /* eslint-disable @stylistic/js/array-element-newline */
        assert.deepEqual(
          shapeGroupKid.attrs.points,
          [
            126, 40, 58, 80, 60, 116, 92, 128, 93, 143, 93,
            151, 94, 151, 114, 150, 128, 150, 214, 135, 183,
            56, 182, 56, 182, 56
          ],
          'Line has the proper points.');
        /* eslint-enable @stylistic/js/array-element-newline */
        const colour = (version === '0.1' ? '#ffff00' : '#ffff80');
        assert.equal(
          shapeGroupKid.attrs.stroke,
          colour,
          'Line has the proper colour.');
      } else if (shapeGroupKid.className === 'Label') {
        hasLabel = true;
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'label\' must not be draggable.');
        assert.equal(shapeGroupKid.children.length, 2, 'Label has 2 kids.');
        const labelGroupKid0 = shapeGroupKid.children[0];
        assert.equal(
          labelGroupKid0.className,
          'Text',
          'Label group first level is a text.');
        if (version !== '0.1') {
          assert.equal(
            labelGroupKid0.attrs.text,
            'Brain',
            'Text has the proper value.');
        }
        const labelGroupKid1 = shapeGroupKid.children[1];
        assert.equal(
          labelGroupKid1.className,
          'Tag',
          'Label group second level is a tag.');
      }
    }
    assert.ok(hasShape, 'Shape group contains a shape.');
    assert.ok(hasLabel, 'Shape group contains a label.');

    // details
    if (version !== '0.1') {
      const details0 = details['4l24ofouhmf'];
      assert.equal(
        details0.meta.textExpr,
        'Brain',
        'Details textExpr has the proper value.');
      if (version === '0.2' || version === '0.3') {
        assert.equal(
          details0.meta.longText,
          'This is a squary brain!',
          'Details longText has the proper value.');
      }
    }
  }

  /**
   * Check a hand drawing.
   *
   * @param {object} posGroupKid The position group (only) kid.
   * @param {object} details The drawing details.
   * @param {string} version The state format version.
   */
  function checkHandDrawing(posGroupKid, details, version) {
    // check group
    assert.equal(
      posGroupKid.attrs.name,
      'freeHand-group',
      'Shape group is a freeHand group.');
    assert.ok(posGroupKid.attrs.draggable, 'Shape group must be draggable.');
    assert.equal(
      posGroupKid.attrs.id,
      '08m011yjp8je',
      'Position group first level has the proper id.');
    assert.notEqual(
      typeof details['08m011yjp8je'],
      'undefined',
      'Details should contain data for id.');

    // kids
    assert.equal(posGroupKid.children.length, 2, 'Shape group has 2 kids.');
    let hasShape = false;
    let hasLabel = false;
    for (let i = 0; i < posGroupKid.children.length; ++i) {
      const shapeGroupKid = posGroupKid.children[i];
      if (shapeGroupKid.attrs.name === 'shape') {
        hasShape = true;
        assert.equal(
          shapeGroupKid.className,
          'Line',
          'Shape group \'shape\' is a line.');
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'shape\' must not be draggable.');
        /* eslint-disable @stylistic/js/array-element-newline */
        /* eslint-disable @stylistic/js/max-len */
        assert.deepEqual(
          shapeGroupKid.attrs.points,
          [
            93, 50, 92, 50, 71, 58, 71, 59, 68, 63, 68, 64, 54, 80, 54, 81, 53,
            83, 52, 85, 51, 87, 50, 92, 47, 98, 47, 100, 55, 117, 56, 118, 86,
            122, 89, 126, 90, 128, 90, 129, 88, 131, 87, 133, 86, 134, 85, 138,
            85, 139, 85, 141, 87, 143, 87, 144, 87, 145, 88, 147, 92, 150, 97,
            152, 112, 154, 113, 153, 136, 143, 174, 136, 178, 135, 191, 136, 192,
            136, 194, 138, 196, 139, 197, 141, 199, 141, 200, 141, 207, 140, 208,
            139, 211, 136, 213, 134, 213, 133, 213, 129, 213, 126, 213, 122, 210,
            90, 209, 87, 206, 80, 205, 79, 204, 78, 201, 73, 199, 71, 197, 70,
            187, 63, 184, 61, 182, 59, 180, 58, 174, 54, 172, 52, 170, 51, 168,
            50, 167, 49, 155, 42, 154, 42, 151, 42, 136, 40, 134, 40, 133, 40,
            128, 41, 126, 41, 124, 41, 117, 44, 116, 44, 114, 44, 107, 47, 102,
            49, 100, 49, 97, 49, 93, 50, 92, 50, 92, 50
          ],
          'Line has the proper points.');
        /* eslint-enable @stylistic/js/max-len */
        /* eslint-enable @stylistic/js/array-element-newline */
        assert.equal(
          shapeGroupKid.attrs.stroke,
          '#ffff80',
          'Line has the proper colour.');
      } else if (shapeGroupKid.className === 'Label') {
        hasLabel = true;
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'label\' must not be draggable.');
        assert.equal(shapeGroupKid.children.length, 2, 'Label has 2 kids.');
        const labelGroupKid0 = shapeGroupKid.children[0];
        assert.equal(
          labelGroupKid0.className,
          'Text',
          'Label group first level is a text.');
        assert.equal(
          labelGroupKid0.attrs.text,
          'Brain',
          'Text has the proper value.');
        const labelGroupKid1 = shapeGroupKid.children[1];
        assert.equal(
          labelGroupKid1.className,
          'Tag',
          'Label group second level is a tag.');
      }
    }
    assert.ok(hasShape, 'Shape group contains a shape.');
    assert.ok(hasLabel, 'Shape group contains a label.');

    // details
    const details0 = details['08m011yjp8je'];
    assert.equal(
      details0.meta.textExpr,
      'Brain',
      'Details textExpr has the proper value.');
    if (version === '0.2' || version === '0.3') {
      assert.equal(
        details0.meta.longText,
        'This is a roundy brain!',
        'Details longText has the proper value.');
    }
  }

  /**
   * Check an ellipse drawing.
   *
   * @param {object} posGroupKid The position group (only) kid.
   * @param {object} details The drawing details.
   * @param {string} version The state format version.
   */
  function checkEllipseDrawing(
    posGroupKid, details, version) {
    // check group
    assert.equal(
      posGroupKid.attrs.name,
      'ellipse-group',
      'Shape group is an ellipse group.');
    assert.ok(posGroupKid.attrs.draggable, 'Shape group must be draggable.');
    assert.equal(
      posGroupKid.attrs.id,
      'c6j16qt6vt6',
      'Position group first level has the proper id.');
    assert.notEqual(
      typeof details.c6j16qt6vt6,
      'undefined',
      'Details should contain data for id.');

    // kids
    assert.equal(posGroupKid.children.length, 2, 'Shape group has 2 kids.');
    let hasShape = false;
    let hasLabel = false;
    for (let i = 0; i < posGroupKid.children.length; ++i) {
      const shapeGroupKid = posGroupKid.children[i];
      if (shapeGroupKid.attrs.name === 'shape') {
        hasShape = true;
        assert.equal(
          shapeGroupKid.className,
          'Ellipse',
          'Shape group \'shape\' is an ellipse.');
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'shape\' must not be draggable.');
        assert.deepEqual(shapeGroupKid.attrs.x, 90,
          'Ellipse has the proper x.');
        assert.deepEqual(shapeGroupKid.attrs.y, 78,
          'Ellipse has the proper y.');
        assert.deepEqual(
          shapeGroupKid.attrs.radiusX,
          53,
          'Ellipse has the proper radiusX.');
        assert.deepEqual(
          shapeGroupKid.attrs.radiusY,
          32,
          'Ellipse has the proper radiusY.');
        const colour = (version === '0.1' ? '#ffff00' : '#ffff80');
        assert.equal(
          shapeGroupKid.attrs.stroke,
          colour,
          'Ellipse has the proper colour.');
      } else if (shapeGroupKid.className === 'Label') {
        hasLabel = true;
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'label\' must not be draggable.');
        assert.equal(shapeGroupKid.children.length, 2, 'Label has 2 kids.');
        const labelGroupKid0 = shapeGroupKid.children[0];
        assert.equal(
          labelGroupKid0.className,
          'Text',
          'Label group first level is a text.');
        const unit = parseFloat(version) <= 0.3 ? 'cm2' : ' cm²';
        assert.equal(
          labelGroupKid0.attrs.text,
          '53.28' + unit,
          'Text has the proper value.');
        const labelGroupKid1 = shapeGroupKid.children[1];
        assert.equal(
          labelGroupKid1.className,
          'Tag',
          'Label group second level is a tag.');
      }
    }
    assert.ok(hasShape, 'Shape group contains a shape.');
    assert.ok(hasLabel, 'Shape group contains a label.');

    // details
    if (version !== '0.1') {
      const details0 = details.c6j16qt6vt6;
      assert.equal(
        details0.meta.textExpr,
        '{surface}',
        'Details textExpr has the proper value.');
      if (version === '0.2' || version === '0.3') {
        assert.equal(
          details0.meta.longText,
          'What a surface!',
          'Details longText has the proper value.');
      }
    }
  }

  /**
   * Check a protractor drawing.
   *
   * @param {object} posGroupKid The position group (only) kid.
   * @param {object} details The drawing details.
   * @param {string} version The state format version.
   */
  function checkProtractorDrawing(
    posGroupKid, details, version) {
    // check group
    assert.equal(
      posGroupKid.attrs.name,
      'protractor-group',
      'Shape group is an protractor group.');
    assert.ok(posGroupKid.attrs.draggable, 'Shape group must be draggable.');
    assert.equal(
      posGroupKid.attrs.id,
      '49g7kqi3p4u',
      'Position group first level has the proper id.');
    assert.notEqual(
      typeof details['49g7kqi3p4u'],
      'undefined',
      'Details should contain data for id.');

    // kids
    assert.equal(posGroupKid.children.length, 3, 'Shape group has 3 kids.');
    let hasShape = false;
    let hasLabel = false;
    let hasArc = false;
    for (let i = 0; i < posGroupKid.children.length; ++i) {
      const shapeGroupKid = posGroupKid.children[i];
      if (shapeGroupKid.attrs.name === 'shape') {
        hasShape = true;
        assert.equal(
          shapeGroupKid.className,
          'Line',
          'Shape group \'shape\' is a line.');
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'shape\' must not be draggable.');
        assert.deepEqual(
          shapeGroupKid.attrs.points,
          [33, 164, 81, 145, 93, 198],
          'Line has the proper points.');
        const colour = (version === '0.1' ? '#ffff00' : '#ffff80');
        assert.equal(
          shapeGroupKid.attrs.stroke,
          colour,
          'Line has the proper colour.');
      } else if (shapeGroupKid.className === 'Label') {
        hasLabel = true;
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'label\' must not be draggable.');
        assert.equal(shapeGroupKid.children.length, 2, 'Label has 2 kids.');
        const labelGroupKid0 = shapeGroupKid.children[0];
        assert.equal(
          labelGroupKid0.className,
          'Text',
          'Label group first level is a text.');
        const unit = parseFloat(version) <= 0.3 ? '°' : ' °';
        assert.equal(
          labelGroupKid0.attrs.text,
          '80.15' + unit,
          'Text has the proper value.');
        const labelGroupKid1 = shapeGroupKid.children[1];
        assert.equal(
          labelGroupKid1.className,
          'Tag',
          'Label group second level is a tag.');
      } else if (shapeGroupKid.className === 'Arc') {
        hasArc = true;
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'extra\' must not be draggable.');
      }
    }
    assert.ok(hasShape, 'Shape group contains a shape.');
    assert.ok(hasLabel, 'Shape group contains a label.');
    assert.ok(hasArc, 'Shape group contains an arc.');

    // details
    if (version !== '0.1') {
      const details0 = details['49g7kqi3p4u'];
      assert.equal(
        details0.meta.textExpr,
        '{angle}',
        'Details textExpr has the proper value.');
      if (version === '0.2' || version === '0.3') {
        assert.equal(
          details0.meta.longText,
          'What an angle!',
          'Details longText has the proper value.');
      }
    }
  }

  /**
   * Check a rectangle drawing.
   *
   * @param {object} posGroupKid The position group (only) kid.
   * @param {object} details The drawing details.
   * @param {string} version The state format version.
   */
  function checkRectangleDrawing(
    posGroupKid, details, version) {
    // check group
    assert.equal(
      posGroupKid.attrs.name,
      'rectangle-group',
      'Shape group is a rectangle group.');
    assert.ok(posGroupKid.attrs.draggable, 'Shape group must be draggable.');
    assert.equal(
      posGroupKid.attrs.id,
      'db0puu209qe',
      'Position group first level has the proper id.');
    assert.notEqual(
      typeof details.db0puu209qe,
      'undefined',
      'Details should contain data for id.');

    // kids
    assert.equal(posGroupKid.children.length, 2, 'Shape group has 2 kids.');
    let hasShape = false;
    let hasLabel = false;
    for (let i = 0; i < posGroupKid.children.length; ++i) {
      const shapeGroupKid = posGroupKid.children[i];
      if (shapeGroupKid.attrs.name === 'shape') {
        hasShape = true;
        assert.equal(
          shapeGroupKid.className,
          'Rect',
          'Shape group \'shape\' is a rectangle.');
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'shape\' must not be draggable.');
        assert.deepEqual(
          shapeGroupKid.attrs.x,
          80,
          'Rectangle has the proper x.');
        assert.deepEqual(
          shapeGroupKid.attrs.y,
          58,
          'Rectangle has the proper y.');
        assert.deepEqual(
          shapeGroupKid.attrs.width,
          104,
          'Rectangle has the proper width.');
        assert.deepEqual(
          shapeGroupKid.attrs.height,
          64,
          'Rectangle has the proper height.');
        const colour = (version === '0.1' ? '#ffff00' : '#ffff80');
        assert.equal(
          shapeGroupKid.attrs.stroke,
          colour,
          'Rectangle has the proper colour.');
      } else if (shapeGroupKid.className === 'Label') {
        hasLabel = true;
        assert.notOk(
          shapeGroupKid.attrs.draggable,
          'Shape group \'label\' must not be draggable.');
        assert.equal(shapeGroupKid.children.length, 2, 'Label has 2 kids.');
        const labelGroupKid0 = shapeGroupKid.children[0];
        assert.equal(
          labelGroupKid0.className,
          'Text',
          'Label group first level is a text.');
        const unit = parseFloat(version, 10) <= 0.3 ? 'cm2' : ' cm²';
        assert.equal(
          labelGroupKid0.attrs.text,
          '66.56' + unit,
          'Text has the proper value.');
        const labelGroupKid1 = shapeGroupKid.children[1];
        assert.equal(
          labelGroupKid1.className,
          'Tag',
          'Label group second level is a tag.');
      }
    }
    assert.ok(hasShape, 'Shape group contains a shape.');
    assert.ok(hasLabel, 'Shape group contains a label.');

    // details
    if (version !== '0.1') {
      const details0 = details.db0puu209qe;
      assert.equal(
        details0.meta.textExpr,
        '{surface}',
        'Details textExpr has the proper value.');
      if (version === '0.2' || version === '0.3') {
        assert.equal(
          details0.meta.longText,
          'What a rectangle!',
          'Details longText has the proper value.');
      }
    }
  }

  /**
   * Tests for {@link State} v0.1 containing a line.
   *
   * @function module:tests/state~read-v01-state-line
   */
  test('Read v01 state line', () => {
    testState(v01Line, '0.1', 'line');
  });

  /**
   * Tests for {@link State} v0.1 containing a roi.
   *
   * @function module:tests/state~read-v01-state-roi
   */
  test('Read v01 state roi', () => {
    testState(v01Roi, '0.1', 'roi');
  });

  /**
   * Tests for {@link State} v0.1 containing an ellipse.
   *
   * @function module:tests/state~read-v01-state-ellipse
   */
  test('Read v01 state ellipse', () => {
    testState(v01Ellipse, '0.1', 'ellipse');
  });

  /**
   * Tests for {@link State} v0.1 containing a protractor.
   *
   * @function module:tests/state~read-v01-state-protractor
   */
  test('Read v01 state protractor', () => {
    testState(v01Protractor, '0.1', 'protractor');
  });

  /**
   * Tests for {@link State} v0.1 containing a rectangle.
   *
   * @function module:tests/state~read-v01-state-rectangle
   */
  test('Read v01 state rectangle', () => {
    testState(v01Rectangle, '0.1', 'rectangle');
  });

  /**
   * Tests for {@link State} v0.1 containing a multi slice ruler.
   *
   * @function module:tests/state~read-v01-state-line-multi-slice
   */
  test('Read v01 state line multi-slice', () => {
    testState(v01LineMulti, '0.1', 'line_multi-slice');
  });

  /**
   * Tests for {@link State} v0.2 containing an arrow.
   *
   * @function module:tests/state~read-v02-state-arrow
   */
  test('Read v02 state arrow', () => {
    testState(v02Arrow, '0.2', 'arrow');
  });

  /**
   * Tests for {@link State} v0.2 containing a ruler.
   *
   * @function module:tests/state~read-v02-state-ruler
   */
  test('Read v02 state ruler', () => {
    testState(v02Ruler, '0.2', 'ruler');
  });

  /**
   * Tests for {@link State} v0.2 containing a roi.
   *
   * @function module:tests/state~read-v02-state-roi
   */
  test('Read v02 state roi', () => {
    testState(v02Roi, '0.2', 'roi');
  });

  /**
   * Tests for {@link State} v0.2 containing a hand draw.
   *
   * @function module:tests/state~read-v02-state-hand
   */
  test('Read v02 state hand', () => {
    testState(v02Hand, '0.2', 'hand');
  });

  /**
   * Tests for {@link State} v0.2 containing an ellipse.
   *
   * @function module:tests/state~read-v02-state-ellipse
   */
  test('Read v02 state ellipse', () => {
    testState(v02Ellipse, '0.2', 'ellipse');
  });

  /**
   * Tests for {@link State} v0.2 containing a protractor.
   *
   * @function module:tests/state~read-v02-state-protractor
   */
  test('Read v02 state protractor', () => {
    testState(v02Protractor, '0.2', 'protractor');
  });

  /**
   * Tests for {@link State} v0.2 containing a rectangle.
   *
   * @function module:tests/state~read-v02-state-rectangle
   */
  test('Read v02 state rectangle', () => {
    testState(v02Rectangle, '0.2', 'rectangle');
  });

  /**
   * Tests for {@link State} v0.2 containing a multi slice ruler.
   *
   * @function module:tests/state~read-v02-state-ruler-multi-slice
   */
  test('Read v02 state ruler multi-slice', () => {
    testState(v02RulerMulti, '0.2', 'ruler_multi-slice');
  });

  /**
   * Tests for {@link State} v0.3 containing an arrow.
   *
   * @function module:tests/state~read-v03-state-arrow
   */
  test('Read v03 state arrow', () => {
    testState(v03Arrow, '0.3', 'arrow');
  });

  /**
   * Tests for {@link State} v0.3 containing a ruler.
   *
   * @function module:tests/state~read-v03-state-ruler
   */
  test('Read v03 state ruler', () => {
    testState(v03Ruler, '0.3', 'ruler');
  });

  /**
   * Tests for {@link State} v0.3 containing a roi.
   *
   * @function module:tests/state~read-v03-state-roi
   */
  test('Read v03 state roi', () => {
    testState(v03Roi, '0.3', 'roi');
  });

  /**
   * Tests for {@link State} v0.3 containing a hand draw.
   *
   * @function module:tests/state~read-v03-state-hand
   */
  test('Read v03 state hand', () => {
    testState(v03Hand, '0.3', 'hand');
  });

  /**
   * Tests for {@link State} v0.3 containing an ellipse.
   *
   * @function module:tests/state~read-v03-state-ellipse
   */
  test('Read v03 state ellipse', () => {
    testState(v03Ellipse, '0.3', 'ellipse');
  });

  /**
   * Tests for {@link State} v0.3 containing a protractor.
   *
   * @function module:tests/state~read-v03-state-protractor
   */
  test('Read v03 state protractor', () => {
    testState(v03Protractor, '0.3', 'protractor');
  });

  /**
   * Tests for {@link State} v0.3 containing a rectangle.
   *
   * @function module:tests/state~read-v03-state-rectangle
   */
  test('Read v03 state rectangle', () => {
    testState(v03Rectangle, '0.3', 'rectangle');
  });

  /**
   * Tests for {@link State} v0.3 containing a multi slice ruler.
   *
   * @function module:tests/state~read-v03-state-ruler-multi-slice
   */
  test('Read v03 state ruler multi-slice', () => {
    testState(v03RulerMulti, '0.3', 'ruler_multi-slice');
  });

  /**
   * Tests for {@link State} v0.4 containing an arrow.
   *
   * @function module:tests/state~read-v04-state-arrow
   */
  test('Read v04 state arrow', () => {
    testState(v04Arrow, '0.4', 'arrow');
  });

  /**
   * Tests for {@link State} v0.4 containing a ruler.
   *
   * @function module:tests/state~read-v04-state-ruler
   */
  test('Read v04 state ruler', () => {
    testState(v04Ruler, '0.4', 'ruler');
  });

  /**
   * Tests for {@link State} v0.4 containing a roi.
   *
   * @function module:tests/state~read-v04-state-roi
   */
  test('Read v04 state roi', () => {
    testState(v04Roi, '0.4', 'roi');
  });

  /**
   * Tests for {@link State} v0.4 containing a hand draw.
   *
   * @function module:tests/state~read-v04-state-hand
   */
  test('Read v04 state hand', () => {
    testState(v04Hand, '0.4', 'hand');
  });

  /**
   * Tests for {@link State} v0.4 containing an ellipse.
   *
   * @function module:tests/state~read-v04-state-ellipse
   */
  test('Read v04 state ellipse', () => {
    testState(v04Ellipse, '0.4', 'ellipse');
  });

  /**
   * Tests for {@link State} v0.4 containing a protractor.
   *
   * @function module:tests/state~read-v04-state-protractor
   */
  test('Read v04 state protractor', () => {
    testState(v04Protractor, '0.4', 'protractor');
  });


  /**
   * Tests for {@link State} v0.4 containing a rectangle.
   *
   * @function module:tests/state~read-v04-state-rectangle
   */
  test('Read v04 state rectangle', () => {
    testState(v04Rectangle, '0.4', 'rectangle');
  });

  /**
   * Tests for {@link State} v0.4 containing a multi slice ruler.
   *
   * @function module:tests/state~read-v04-state-ruler-multi-slice
   */
  test('Read v04 state ruler multi-slice', () => {
    testState(v04RulerMulti, '0.4', 'ruler_multi-slice');
  });

  /**
   * Tests for {@link State} v0.5 containing an arrow.
   *
   * @function module:tests/state~read-v05-state-arrow
   */
  test('Read v05 state arrow', () => {
    testState(v05Arrow, '0.5', 'arrow');
  });

  /**
   * Tests for {@link State} v0.5 containing a ruler.
   *
   * @function module:tests/state~read-v05-state-ruler
   */
  test('Read v05 state ruler', () => {
    testState(v05Ruler, '0.5', 'ruler');
  });

  /**
   * Tests for {@link State} v0.5 containing a roi.
   *
   * @function module:tests/state~read-v05-state-roi
   */
  test('Read v05 state roi', () => {
    testState(v05Roi, '0.5', 'roi');
  });

  /**
   * Tests for {@link State} v0.5 containing a hand draw.
   *
   * @function module:tests/state~read-v05-state-hand
   */
  test('Read v05 state hand', () => {
    testState(v05Hand, '0.5', 'hand');
  });

  /**
   * Tests for {@link State} v0.5 containing an ellipse.
   *
   * @function module:tests/state~read-v05-state-ellipse
   */
  test('Read v05 state ellipse', () => {
    testState(v05Ellipse, '0.5', 'ellipse');
  });

  /**
   * Tests for {@link State} v0.5 containing a protractor.
   *
   * @function module:tests/state~read-v05-state-protractor
   */
  test('Read v05 state protractor', () => {
    testState(v05Protractor, '0.5', 'protractor');
  });


  /**
   * Tests for {@link State} v0.5 containing a rectangle.
   *
   * @function module:tests/state~read-v05-state-rectangle
   */
  test('Read v05 state rectangle', () => {
    testState(v05Rectangle, '0.5', 'rectangle');
  });

  /**
   * Tests for {@link State} v0.5 containing a multi slice ruler.
   *
   * @function module:tests/state~read-v05-state-ruler-multi-slice
   */
  test('Read v05 state ruler multi-slice', () => {
    testState(v05RulerMulti, '0.5', 'ruler_multi-slice');
  });

});