Home Manual Reference Source Test

test/phylogenies.js

/*
 * Test phylogenies.
 */

// Load phyx.js, our PHYX library, and chai for testing.
const chai = require('chai');
const phyx = require('../src');

// Make it easier to access owlterms.
const owlterms = require('../src/utils/owlterms');

// Use Chai's expect API for testing.
const expect = chai.expect;

/*
 * These tests focus on three aspects of PhylogenyWrapper:
 *  - Whether it can detect errors in an input Newick string.
 *  - Retrieve taxonomic units from the phylogeny based on either their node label
 *    or on the additional properties associated with the phylogeny.
 *  - Whether we can match specifiers with nodes on the phylogeny if they share
 *    taxonomic units that match.
 */

describe('PhylogenyWrapper', function () {
  describe('#constructor', function () {
    describe('when used to wrap an empty object', function () {
      it('should return a PhylogenyWrapper object', function () {
        expect(new phyx.PhylogenyWrapper({}))
          .to.be.an.instanceOf(phyx.PhylogenyWrapper);
      });
    });
  });

  describe('#getErrorsInNewickString', function () {
    describe('when given a correct Newick string', function () {
      const correctNewickStrings = [
        '(A:3, B:5, (C:6, N:7));',
      ];

      it('should return an empty list of errors', function () {
        correctNewickStrings.forEach((str) => {
          expect(phyx.PhylogenyWrapper.getErrorsInNewickString(str)).to.be.empty;
        });
      });
    });

    describe('when given an empty Newick string', function () {
      const emptyNewickStrings = [
        '()',
        '();  ',
      ];

      it('should return a single "No phylogeny entered" error', function () {
        emptyNewickStrings.forEach((newick) => {
          const errors = phyx.PhylogenyWrapper.getErrorsInNewickString(newick);
          expect(errors).to.have.length(1);
          expect(errors[0].title).to.equal('No phylogeny entered');
        });
      });
    });

    describe('when given an unbalanced Newick string', function () {
      const unbalancedNewickString = [
        {
          newick: '(A, B))',
          expected: 'You have 1 too few open parentheses',
        },
        {
          newick: '(A, (B, (C, D))',
          expected: 'You have 1 too many open parentheses',
        },
        {
          newick: '(A, (B, (C, (((D))',
          expected: 'You have 4 too many open parentheses',
        },
      ];

      it('should report how many parentheses are missing', function () {
        unbalancedNewickString.forEach((entry) => {
          const errors = phyx.PhylogenyWrapper.getErrorsInNewickString(entry.newick);

          // We should get two errors.
          expect(errors).to.have.lengthOf(2);

          // Should include an error about the unbalanced parentheses.
          expect(errors[0].title).to.equal('Unbalanced parentheses in Newick string');
          expect(errors[0].message).to.equal(entry.expected);

          // Should include an error passed on from the Newick parser.
          expect(errors[1].title).to.equal('Error parsing phylogeny');
          expect(errors[1].message).to.include('An error occured while parsing this phylogeny:');
        });
      });
    });

    describe('when given an incomplete Newick string', function () {
      const incompleteNewickStrings = [
        ';',
        '))(A, (B, ',
      ];

      it('should report an error parsing the phylogeny', function () {
        incompleteNewickStrings.forEach((newick) => {
          const errors = phyx.PhylogenyWrapper.getErrorsInNewickString(newick);

          expect(errors).to.have.lengthOf(1);
          expect(errors[0].title).to.equal('Error parsing phylogeny');
          expect(errors[0].message).to.include('An error occured while parsing this phylogeny:');
        });
      });
    });
  });

  describe('#getNodeLabels', function () {
    const tests = [
      {
        // Note that 'newick' is the input for this test.
        newick: '(A, (B, (C, D))E, F, (G, (H, I, J)K, L)M, N)O',
        // The following keys indicate the expected all/internal/terminal node labels
        // for the given Newick string.
        nodeLabels: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'],
        internalNodeLabels: ['E', 'K', 'M', 'O'],
        terminalNodeLabels: ['A', 'B', 'C', 'D', 'F', 'G', 'H', 'I', 'J', 'L', 'N'],
      },
    ];

    tests.forEach((test) => {
      const wrapper = new phyx.PhylogenyWrapper({ newick: test.newick });

      describe('For a particular Newick phylogeny', function () {
        it('should return a list of all node labels by default', function () {
          expect(wrapper.getNodeLabels().sort())
            .to.have.members(test.nodeLabels.sort());
        });

        it('should return a list of internal labels when asked for internal labels', function () {
          expect(wrapper.getNodeLabels('internal').sort())
            .to.have.members(test.internalNodeLabels.sort());
        });

        it('should return a list of terminal labels when asked for terminal labels', function () {
          expect(wrapper.getNodeLabels('terminal').sort())
            .to.have.members(test.terminalNodeLabels.sort());
        });
      });
    });
  });

  describe('given a particular phylogeny with additional node properties', function () {
    const wrapper = new phyx.PhylogenyWrapper({
      newick: '((MVZ225749, MVZ191016), Rana boylii)',
      additionalNodeProperties: {
        MVZ225749: {
          representsTaxonomicUnits: [{
            '@type': [
              phyx.TaxonomicUnitWrapper.TYPE_SPECIMEN,
            ],
            nameString: 'Rana luteiventris',
            occurrenceID: 'MVZ:225749',
          }],
        },
        MVZ191016: {
          representsTaxonomicUnits: [{
            '@type': [
              phyx.TaxonomicUnitWrapper.TYPE_SPECIMEN,
            ],
            nameString: 'Rana luteiventris',
            occurrenceID: 'MVZ:191016',
          }],
        },
      },
    });

    describe('#getNodeLabels', function () {
      it('should return the list of node labels from the Newick string', function () {
        expect(wrapper.getNodeLabels().sort())
          .to.have.members([
            'MVZ191016',
            'MVZ225749',
            'Rana boylii',
          ]);
      });
    });

    describe('#getTaxonomicUnitsForNodeLabel', function () {
      it('should return the list of taxonomic units using information from additional node properties', function () {
        expect(wrapper.getTaxonomicUnitsForNodeLabel('MVZ191016')).to.deep.equal([{
          '@type': [
            phyx.TaxonomicUnitWrapper.TYPE_SPECIMEN,
          ],
          nameString: 'Rana luteiventris',
          occurrenceID: 'MVZ:191016',
        }]);

        expect(wrapper.getTaxonomicUnitsForNodeLabel('MVZ225749')).to.deep.equal([{
          '@type': [
            phyx.TaxonomicUnitWrapper.TYPE_SPECIMEN,
          ],
          nameString: 'Rana luteiventris',
          occurrenceID: 'MVZ:225749',
        }]);

        expect(wrapper.getTaxonomicUnitsForNodeLabel('Rana boylii')).to.deep.equal([{
          '@type': owlterms.TDWG_VOC_TAXON_CONCEPT,
          label: 'Rana boylii',
          hasName: {
            '@type': 'http://rs.tdwg.org/ontology/voc/TaxonName#TaxonName',
            label: 'Rana boylii',
            nameComplete: 'Rana boylii',
            genusPart: 'Rana',
            specificEpithet: 'boylii',
          },
        }]);
      });
    });

    describe('#getNodeLabelsMatchedBySpecifier', function () {
      it('should match a specifier to MVZ225749 based on occurrence ID', function () {
        const specifier1 = {
          '@type': phyx.TaxonomicUnitWrapper.TYPE_SPECIMEN,
          occurrenceID: 'MVZ:225749',
        };
        expect(wrapper.getNodeLabelsMatchedBySpecifier(specifier1))
          .to.have.members(['MVZ225749']);
      });

      it('should match a specifier to MVZ191016 based on occurrence ID', function () {
        const specifier2 = {
          '@type': phyx.TaxonomicUnitWrapper.TYPE_SPECIMEN,
          occurrenceID: 'MVZ:191016',
        };

        expect(wrapper.getNodeLabelsMatchedBySpecifier(specifier2))
          .to.have.members(['MVZ191016']);
      });

      it('should match a specifier to node "Rana boylii" based on the parsed scientific name', function () {
        const specifier3 = {
          '@type': phyx.TaxonomicUnitWrapper.TYPE_TAXON_CONCEPT,
          nameString: 'Rana boylii',
        };

        expect(wrapper.getNodeLabelsMatchedBySpecifier(specifier3))
          .to.have.members(['Rana boylii']);
      });
    });
  });

  describe('#asJSONLD', function () {
    it('should preserve an existing @id on input phylogenies', function () {
      const jsonld = new phyx.PhylogenyWrapper({
        '@id': '#providedId',
        newick: '((Homo_sapiens, Panthera_tigris), Mus_musculus)',
      }).asJSONLD('#phylogeny0');
      expect(jsonld).to.have.property('@id');
      expect(jsonld['@id']).to.equal('#providedId');
    });
    it('should generate a new @id on input phylorefs', function () {
      const jsonld = new phyx.PhylogenyWrapper({
        newick: '((Homo_sapiens, Panthera_tigris), Mus_musculus)',
      }, owlterms.ICZN_CODE).asJSONLD('#phylogeny0');
      expect(jsonld).to.have.property('@id');
      expect(jsonld['@id']).to.equal('#phylogeny0');
    });
    it('should generate the phylogeny in JSON-LD as expected', function () {
      const expectedResults = [
        {
          newick: '((Homo_sapiens, Panthera_tigris), Mus_musculus)',
          jsonld: {
            '@id': '#',
            '@type': 'phyloref:ReferencePhylogenyEvidence',
            hasRootNode: { '@id': '#_node0' },
            newick: '((Homo_sapiens, Panthera_tigris), Mus_musculus)',
            nodes: [
              {
                '@id': '#_node0',
                children: ['#_node1', '#_node2'],
                'rdf:type': [{ '@id': owlterms.CDAO_NODE }],
              },
              {
                '@id': '#_node1',
                'rdf:type': [
                  { '@id': owlterms.CDAO_NODE },
                  {
                    '@type': owlterms.OWL_RESTRICTION,
                    onProperty: owlterms.CDAO_REPRESENTS_TU,
                    someValuesFrom: {
                      '@type': owlterms.OWL_RESTRICTION,
                      onProperty: owlterms.TDWG_VOC_HAS_NAME,
                      someValuesFrom: {
                        '@type': owlterms.OWL_CLASS,
                        intersectionOf: [{
                          '@type': owlterms.OWL_RESTRICTION,
                          hasValue: 'Mus musculus',
                          onProperty: owlterms.TDWG_VOC_NAME_COMPLETE,
                        }, {
                          '@type': owlterms.OWL_RESTRICTION,
                          hasValue: {
                            '@id': owlterms.ICZN_CODE,
                          },
                          onProperty: owlterms.NOMENCLATURAL_CODE,
                        }],
                      },
                    },
                  },
                ],
                labels: ['Mus_musculus'],
                parent: '#_node0',
                representsTaxonomicUnits: [{
                  '@type': owlterms.TDWG_VOC_TAXON_CONCEPT,
                  hasName: {
                    '@type': owlterms.TDWG_VOC_TAXON_NAME,
                    genusPart: 'Mus',
                    label: 'Mus_musculus',
                    nameComplete: 'Mus musculus',
                    nomenclaturalCode: owlterms.ICZN_CODE,
                    specificEpithet: 'musculus',
                  },
                  label: 'Mus_musculus',
                }],
                siblings: ['#_node2'],
              },
              {
                '@id': '#_node2',
                children: ['#_node3', '#_node4'],
                'rdf:type': [{ '@id': owlterms.CDAO_NODE }],
                parent: '#_node0',
                siblings: ['#_node1'],
              },
              {
                '@id': '#_node3',
                'rdf:type': [
                  { '@id': owlterms.CDAO_NODE },
                  {
                    '@type': owlterms.OWL_RESTRICTION,
                    onProperty: owlterms.CDAO_REPRESENTS_TU,
                    someValuesFrom: {
                      '@type': owlterms.OWL_RESTRICTION,
                      onProperty: owlterms.TDWG_VOC_HAS_NAME,
                      someValuesFrom: {
                        '@type': owlterms.OWL_CLASS,
                        intersectionOf: [{
                          '@type': owlterms.OWL_RESTRICTION,
                          hasValue: 'Panthera tigris',
                          onProperty: owlterms.TDWG_VOC_NAME_COMPLETE,
                        }, {
                          '@type': owlterms.OWL_RESTRICTION,
                          hasValue: {
                            '@id': owlterms.ICZN_CODE,
                          },
                          onProperty: owlterms.NOMENCLATURAL_CODE,
                        }],
                      },
                    },
                  },
                ],
                labels: ['Panthera_tigris'],
                parent: '#_node2',
                representsTaxonomicUnits: [{
                  '@type': owlterms.TDWG_VOC_TAXON_CONCEPT,
                  hasName: {
                    '@type': owlterms.TDWG_VOC_TAXON_NAME,
                    genusPart: 'Panthera',
                    label: 'Panthera_tigris',
                    nameComplete: 'Panthera tigris',
                    nomenclaturalCode: owlterms.ICZN_CODE,
                    specificEpithet: 'tigris',
                  },
                  label: 'Panthera_tigris',
                }],
                siblings: ['#_node4'],
              },
              {
                '@id': '#_node4',
                'rdf:type': [
                  { '@id': owlterms.CDAO_NODE },
                  {
                    '@type': owlterms.OWL_RESTRICTION,
                    onProperty: owlterms.CDAO_REPRESENTS_TU,
                    someValuesFrom: {
                      '@type': owlterms.OWL_RESTRICTION,
                      onProperty: owlterms.TDWG_VOC_HAS_NAME,
                      someValuesFrom: {
                        '@type': owlterms.OWL_CLASS,
                        intersectionOf: [{
                          '@type': owlterms.OWL_RESTRICTION,
                          hasValue: 'Homo sapiens',
                          onProperty: owlterms.TDWG_VOC_NAME_COMPLETE,
                        }, {
                          '@type': owlterms.OWL_RESTRICTION,
                          hasValue: {
                            '@id': owlterms.ICZN_CODE,
                          },
                          onProperty: owlterms.NOMENCLATURAL_CODE,
                        }],
                      },
                    },
                  },
                ],
                labels: ['Homo_sapiens'],
                parent: '#_node2',
                representsTaxonomicUnits: [
                  {
                    '@type': owlterms.TDWG_VOC_TAXON_CONCEPT,
                    hasName: {
                      '@type': owlterms.TDWG_VOC_TAXON_NAME,
                      genusPart: 'Homo',
                      label: 'Homo_sapiens',
                      nameComplete: 'Homo sapiens',
                      nomenclaturalCode: owlterms.ICZN_CODE,
                      specificEpithet: 'sapiens',
                    },
                    label: 'Homo_sapiens',
                  },
                ],
                siblings: ['#_node3'],
              },
            ],
          },
        },
      ];

      expectedResults.forEach((expected) => {
        const wrapper = new phyx.PhylogenyWrapper({
          '@id': '#',
          newick: expected.newick,
        }, owlterms.ICZN_CODE);
        expect(wrapper.asJSONLD('#phylogeny0')).to.deep.equal(expected.jsonld);
      });
    });
  });

  describe('#getParsedNewickWithIRIs', function () {
    const tests = [
      {
        newick: '(((A, B)C, D)E, F)G',
        result: {
          json: {
            '@id': '_node0',
            name: 'G',
            label: 'G',
            children: [
              { '@id': '_node1', name: 'F', label: 'F' },
              {
                '@id': '_node2',
                name: 'E',
                label: 'E',
                children: [
                  { '@id': '_node3', name: 'D', label: 'D' },
                  {
                    '@id': '_node4',
                    name: 'C',
                    label: 'C',
                    children: [
                      { '@id': '_node5', name: 'B', label: 'B' },
                      { '@id': '_node6', name: 'A', label: 'A' },
                    ],
                  },
                ],
              },
            ],
          },
        },
      },
    ];

    tests.forEach((test) => {
      it('should be able to parse a provided Newick string as expected', function () {
        expect(new phyx.PhylogenyWrapper({ newick: test.newick }).getParsedNewickWithIRIs(''))
          .to.deep.equal(test.result);
      });
    });
  });
});