const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js');
const path = require('path');
const extend = require('extend');

describe('libGD.js', function () {
  let gd = null;
  beforeAll((done) =>
    initializeGDevelopJs().then((module) => {
      gd = module;
      done();
    })
  );

  describe('gd.VersionWrapper', function () {
    it('can return the version number of the library', function () {
      expect(typeof gd.VersionWrapper.major()).toBe('number');
      expect(typeof gd.VersionWrapper.minor()).toBe('number');
      expect(typeof gd.VersionWrapper.build()).toBe('number');
      expect(typeof gd.VersionWrapper.revision()).toBe('number');
      expect(typeof gd.VersionWrapper.fullString()).toBe('string');
    });
  });

  describe('gd.Project', function () {
    let project = null;
    beforeAll(() => (project = gd.ProjectHelper.createNewGDJSProject()));

    it('has properties that can be read and changed', function () {
      project.setName('My super project');
      expect(project.getName()).toBe('My super project');
      project.setVersion('1.2.34');
      expect(project.getVersion()).toBe('1.2.34');
      project.setPackageName('com.test.package');
      expect(project.getPackageName()).toBe('com.test.package');
      project.setOrientation('portrait');
      expect(project.getOrientation()).toBe('portrait');
      project.setOrientation('landscape');
      expect(project.getOrientation()).toBe('landscape');
      project.setAuthor('Me');
      expect(project.getAuthor()).toBe('Me');
      project.setMaximumFPS(15);
      expect(project.getMaximumFPS()).toBe(15);
      project.setMinimumFPS(15);
      expect(project.getMinimumFPS()).toBe(15);
      project.setFolderProject(true);
      expect(project.isFolderProject()).toBe(true);
      project.setFolderProject(false);
      expect(project.isFolderProject()).toBe(false);
    });

    it('can store loading screen setup', function () {
      project.getLoadingScreen().showGDevelopSplash(true);
      expect(project.getLoadingScreen().isGDevelopSplashShown()).toBe(true);
      project.getLoadingScreen().showGDevelopSplash(false);
      expect(project.getLoadingScreen().isGDevelopSplashShown()).toBe(false);
    });

    it('handles layouts', function () {
      expect(project.hasLayoutNamed('Scene')).toBe(false);

      project.insertNewLayout('Scene', 0);
      expect(project.hasLayoutNamed('Scene')).toBe(true);
      expect(project.getLayout('Scene').getName()).toBe('Scene');

      project.removeLayout('Scene');
      expect(project.hasLayoutNamed('Scene')).toBe(false);
    });

    it('handles external events', function () {
      expect(project.hasExternalEventsNamed('My events')).toBe(false);

      project.insertNewExternalEvents('My events', 0);
      expect(project.hasExternalEventsNamed('My events')).toBe(true);
      expect(project.getExternalEvents('My events').getName()).toBe(
        'My events'
      );

      project.removeExternalEvents('My events');
      expect(project.hasExternalEventsNamed('My events')).toBe(false);
    });

    it('handles external layouts', function () {
      expect(project.hasExternalLayoutNamed('My layout')).toBe(false);

      project.insertNewExternalLayout('My layout', 0);
      expect(project.hasExternalLayoutNamed('My layout')).toBe(true);
      expect(project.getExternalLayout('My layout').getName()).toBe(
        'My layout'
      );

      project.removeExternalLayout('My layout');
      expect(project.hasExternalLayoutNamed('My layout')).toBe(false);
    });

    it('should validate object names', function () {
      expect(gd.Project.validateName('ThisNameIs_Ok_123')).toBe(true);
      expect(gd.Project.validateName('ThisName IsNot_Ok_123')).toBe(false);
      expect(gd.Project.validateName('ThisNameIsNot_Ok!')).toBe(false);
    });

    it('should have a list of extensions', function () {
      expect(typeof project.getUsedExtensions().size()).toBe('number');
      project.getUsedExtensions().clear();
      expect(project.getUsedExtensions().size()).toBe(0);
    });

    it('handles events functions extensions', function () {
      expect(project.hasEventsFunctionsExtensionNamed('Ext')).toBe(false);

      project.insertNewEventsFunctionsExtension('Ext', 0);
      expect(project.hasEventsFunctionsExtensionNamed('Ext')).toBe(true);
      expect(project.getEventsFunctionsExtension('Ext').getName()).toBe('Ext');

      project.removeEventsFunctionsExtension('Ext');
      expect(project.hasEventsFunctionsExtensionNamed('Ext')).toBe(false);
    });

    afterAll(function () {
      project.delete();
    });
  });

  describe('gd.Layout', function () {
    let project = null;
    let layout = null;
    beforeAll(() => {
      project = gd.ProjectHelper.createNewGDJSProject();
      layout = project.insertNewLayout('Scene', 0);
    });

    it('can have a new name', function () {
      expect(layout.getName()).toBe('Scene');
      layout.setName('My super layout');
      expect(layout.getName()).toBe('My super layout');
    });
    it('can have a name with UTF8 characters', function () {
      layout.setName('Scene with a 官话 name');
      expect(layout.getName()).toBe('Scene with a 官话 name');
    });
    it('can store events', function () {
      var evts = layout.getEvents();
      expect(evts.getEventsCount()).toBe(0);
      var evt = evts.insertNewEvent(
        project,
        'BuiltinCommonInstructions::Standard',
        0
      );
      expect(evts.getEventsCount()).toBe(1);
      evt
        .getSubEvents()
        .insertNewEvent(project, 'BuiltinCommonInstructions::Standard', 0);
      expect(evts.getEventAt(0).getSubEvents().getEventsCount()).toBe(1);
    });
    it('can have objects', function () {
      var object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);
      var object2 = layout.insertNewObject(
        project,
        'TextObject::Text',
        'MyObject2',
        1
      );

      expect(layout.getObjectAt(0).ptr).toBe(object.ptr);
      expect(layout.getObjectAt(1).ptr).toBe(object2.ptr);
      expect(layout.getObjectAt(0).getType()).toBe('Sprite');
      expect(layout.getObjectAt(1).getType()).toBe('TextObject::Text');
    });

    afterAll(function () {
      project.delete();
    });
  });

  describe('gd.Layer', function () {
    it('can have a name and visibility', function () {
      const layer = new gd.Layer();

      layer.setName('GUI');
      layer.setVisibility(false);
      expect(layer.getName()).toBe('GUI');
      expect(layer.getVisibility()).toBe(false);

      layer.delete();
    });
    it('can have effects', function () {
      const layer = new gd.Layer();

      expect(layer.hasEffectNamed('EffectThatDoesNotExist')).toBe(false);
      expect(layer.getEffectsCount()).toBe(0);

      layer.insertNewEffect('MyEffect', 0);
      expect(layer.hasEffectNamed('EffectThatDoesNotExist')).toBe(false);
      expect(layer.hasEffectNamed('MyEffect')).toBe(true);
      expect(layer.getEffectsCount()).toBe(1);
      expect(layer.getEffectPosition('MyEffect')).toBe(0);

      const effect2 = new gd.Effect();
      effect2.setName('MyEffect2');

      layer.insertEffect(effect2, 1);
      expect(layer.hasEffectNamed('MyEffect2')).toBe(true);
      expect(layer.getEffectsCount()).toBe(2);
      expect(layer.getEffectPosition('MyEffect')).toBe(0);
      expect(layer.getEffectPosition('MyEffect2')).toBe(1);

      layer.swapEffects(0, 1);
      expect(layer.getEffectPosition('MyEffect2')).toBe(0);
      expect(layer.getEffectPosition('MyEffect')).toBe(1);

      layer.delete();
    });
    it('can be serialized', function () {
      const layer = new gd.Layer();
      const layer2 = new gd.Layer();

      layer.setName('GUI');
      layer.setVisibility(false);
      layer.insertNewEffect('MyEffect', 0);

      const element = new gd.SerializerElement();
      layer.serializeTo(element);
      layer2.unserializeFrom(element);

      expect(layer2.getName()).toBe('GUI');
      expect(layer2.getVisibility()).toBe(false);
      expect(layer2.getEffectsCount()).toBe(1);
      expect(layer2.getEffectAt(0).getName()).toBe('MyEffect');

      layer.delete();
      layer2.delete();
    });
  });

  describe('gd.Effect', function () {
    it('can have a name, effect name and parameters', function () {
      const effect = new gd.Effect();

      effect.setName('MyEffect');
      effect.setEffectType('Sepia');
      expect(effect.getName()).toBe('MyEffect');
      expect(effect.getEffectType()).toBe('Sepia');

      effect.setDoubleParameter('Brightness', 1);
      effect.setDoubleParameter('Darkness', 0.3);
      effect.setDoubleParameter('Param3', 6);
      expect(effect.getAllDoubleParameters().keys().size()).toBe(3);
      expect(effect.getDoubleParameter('Brightness')).toBe(1);
      expect(effect.getDoubleParameter('Darkness')).toBe(0.3);
      expect(effect.getDoubleParameter('Param3')).toBe(6);

      effect.setStringParameter('SomeImage', 'myImageResource');
      expect(effect.getStringParameter('SomeImage')).toBe('myImageResource');
      expect(effect.getAllStringParameters().keys().size()).toBe(1);

      effect.setBooleanParameter('SomeBoolean', true);
      expect(effect.getBooleanParameter('SomeBoolean')).toBe(true);
      effect.setBooleanParameter('SomeBoolean', false);
      expect(effect.getBooleanParameter('SomeBoolean')).toBe(false);
      expect(effect.getAllBooleanParameters().keys().size()).toBe(1);

      effect.delete();
    });
  });

  describe('gd.ObjectsContainer (using gd.Layout)', function () {
    let project = null;
    beforeAll(() => (project = gd.ProjectHelper.createNewGDJSProject()));

    it('can move objects', function () {
      var layout = project.insertNewLayout('Scene', 0);
      var object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);
      var object2 = layout.insertNewObject(
        project,
        'TextObject::Text',
        'MyObject2',
        1
      );
      var object3 = layout.insertNewObject(
        project,
        'TextObject::Text',
        'MyObject3',
        2
      );

      expect(layout.getObjectAt(0).getName()).toBe('MyObject');
      expect(layout.getObjectAt(1).getName()).toBe('MyObject2');
      expect(layout.getObjectAt(2).getName()).toBe('MyObject3');
      layout.moveObject(0, 2);
      expect(layout.getObjectAt(0).getName()).toBe('MyObject2');
      expect(layout.getObjectAt(1).getName()).toBe('MyObject3');
      expect(layout.getObjectAt(2).getName()).toBe('MyObject');
      layout.moveObject(0, 0);
      expect(layout.getObjectAt(0).getName()).toBe('MyObject2');
      expect(layout.getObjectAt(1).getName()).toBe('MyObject3');
      expect(layout.getObjectAt(2).getName()).toBe('MyObject');
      layout.moveObject(1, 0);
      expect(layout.getObjectAt(0).getName()).toBe('MyObject3');
      expect(layout.getObjectAt(1).getName()).toBe('MyObject2');
      expect(layout.getObjectAt(2).getName()).toBe('MyObject');
      layout.moveObject(0, 999);
      expect(layout.getObjectAt(0).getName()).toBe('MyObject3');
      expect(layout.getObjectAt(1).getName()).toBe('MyObject2');
      expect(layout.getObjectAt(2).getName()).toBe('MyObject');
    });

    it('can find position of objects', function () {
      var layout = project.insertNewLayout('Scene2', 0);
      var object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);
      var object2 = layout.insertNewObject(
        project,
        'TextObject::Text',
        'MyObject2',
        1
      );
      var object3 = layout.insertNewObject(
        project,
        'TextObject::Text',
        'MyObject3',
        2
      );

      expect(layout.getObjectPosition('MyObject')).toBe(0);
      expect(layout.getObjectPosition('MyObject2')).toBe(1);
      expect(layout.getObjectPosition('MyObject3')).toBe(2);
      expect(layout.getObjectPosition('MyObject4')).toBe(-1);
    });

    afterAll(function () {
      project.delete();
    });
  });

  describe('gd.ObjectsContainer', function () {
    it('can move objects between containers, without moving them in memory', function () {
      // Prepare two containers, one with 3 objects and one empty
      const objectsContainer1 = new gd.ObjectsContainer();
      const objectsContainer2 = new gd.ObjectsContainer();
      const mySpriteObject = new gd.SpriteObject('MySprite');
      const mySprite2Object = new gd.SpriteObject('MySprite2');
      const mySprite3Object = new gd.SpriteObject('MySprite3');
      objectsContainer1.insertObject(mySpriteObject, 0);
      objectsContainer1.insertObject(mySprite2Object, 1);
      objectsContainer1.insertObject(mySprite3Object, 2);

      // Objects are copied when inserted in the container, so we delete them:
      mySpriteObject.delete();
      mySprite2Object.delete();
      mySprite3Object.delete();

      // Find the pointer to the objects in memory
      expect(objectsContainer1.getObjectsCount()).toBe(3);
      expect(objectsContainer2.getObjectsCount()).toBe(0);
      const mySpriteObjectPtr = gd.getPointer(objectsContainer1.getObjectAt(0));
      const mySprite2ObjectPtr = gd.getPointer(
        objectsContainer1.getObjectAt(1)
      );
      const mySprite3ObjectPtr = gd.getPointer(
        objectsContainer1.getObjectAt(2)
      );

      // Move objects between containers
      objectsContainer1.moveObjectToAnotherContainer(
        'MySprite2',
        objectsContainer2,
        0
      );
      expect(objectsContainer1.getObjectsCount()).toBe(2);
      expect(objectsContainer1.getObjectAt(0).getName()).toBe('MySprite');
      expect(objectsContainer1.getObjectAt(1).getName()).toBe('MySprite3');
      expect(objectsContainer2.getObjectsCount()).toBe(1);
      expect(objectsContainer2.getObjectAt(0).getName()).toBe('MySprite2');

      objectsContainer1.moveObjectToAnotherContainer(
        'MySprite3',
        objectsContainer2,
        1
      );
      expect(objectsContainer1.getObjectsCount()).toBe(1);
      expect(objectsContainer1.getObjectAt(0).getName()).toBe('MySprite');
      expect(objectsContainer2.getObjectsCount()).toBe(2);
      expect(objectsContainer2.getObjectAt(0).getName()).toBe('MySprite2');
      expect(objectsContainer2.getObjectAt(1).getName()).toBe('MySprite3');

      // Check that the object in memory are the same, even if moved to another container
      expect(gd.getPointer(objectsContainer1.getObjectAt(0))).toBe(
        mySpriteObjectPtr
      );
      expect(gd.getPointer(objectsContainer2.getObjectAt(0))).toBe(
        mySprite2ObjectPtr
      );
      expect(gd.getPointer(objectsContainer2.getObjectAt(1))).toBe(
        mySprite3ObjectPtr
      );

      objectsContainer2.moveObjectToAnotherContainer(
        'MySprite2',
        objectsContainer1,
        0
      );
      expect(objectsContainer1.getObjectsCount()).toBe(2);
      expect(objectsContainer1.getObjectAt(0).getName()).toBe('MySprite2');
      expect(objectsContainer1.getObjectAt(1).getName()).toBe('MySprite');
      expect(objectsContainer2.getObjectsCount()).toBe(1);
      expect(objectsContainer2.getObjectAt(0).getName()).toBe('MySprite3');

      // Check again that the object in memory are the same, even if moved to another container
      expect(gd.getPointer(objectsContainer1.getObjectAt(0))).toBe(
        mySprite2ObjectPtr
      );
      expect(gd.getPointer(objectsContainer1.getObjectAt(1))).toBe(
        mySpriteObjectPtr
      );
      expect(gd.getPointer(objectsContainer2.getObjectAt(0))).toBe(
        mySprite3ObjectPtr
      );
    });
  });

  describe('gd.InitialInstancesContainer', function () {
    let container = null;
    let containerCopy = null;
    beforeAll(() => {
      container = new gd.InitialInstancesContainer();
    });

    it('initial state', function () {
      expect(container.getInstancesCount()).toBe(0);
    });
    it('adding instances', function () {
      var instance = container.insertNewInitialInstance();
      instance.setObjectName('MyObject1');
      instance.setZOrder(10);

      var instance2 = new gd.InitialInstance();
      instance2.setObjectName('MyObject2');
      instance2 = container.insertInitialInstance(instance2);

      var instance3 = container.insertNewInitialInstance();
      instance3.setObjectName('MyObject3');
      instance3.setZOrder(-1);
      instance3.setLayer('OtherLayer');

      expect(container.getInstancesCount()).toBe(3);
    });
    it('iterating', function () {
      var i = 0;
      var functor = new gd.InitialInstanceJSFunctor();
      functor.invoke = function (instance) {
        instance = gd.wrapPointer(instance, gd.InitialInstance);
        expect(
          (i === 0 && instance.getObjectName() === 'MyObject1') ||
            (i === 1 && instance.getObjectName() === 'MyObject2') ||
            (i === 2 && instance.getObjectName() === 'MyObject3')
        ).toBe(true);
        i++;
      };
      container.iterateOverInstances(functor);
    });
    it('can rename instances', function () {
      container.renameInstancesOfObject('MyObject1', 'MyObject');

      var i = 0;
      var functor = new gd.InitialInstanceJSFunctor();
      functor.invoke = function (instance) {
        instance = gd.wrapPointer(instance, gd.InitialInstance);
        expect(
          (i === 0 && instance.getObjectName() === 'MyObject') ||
            (i === 1 && instance.getObjectName() === 'MyObject2') ||
            (i === 2 && instance.getObjectName() === 'MyObject3')
        ).toBe(true);
        i++;
      };
      container.iterateOverInstances(functor);
    });
    it('iterating with z ordering', function () {
      var i = 0;
      var functor = new gd.InitialInstanceJSFunctor();
      functor.invoke = function (instance) {
        instance = gd.wrapPointer(instance, gd.InitialInstance);
        expect(
          (i === 0 && instance.getObjectName() === 'MyObject2') ||
            (i === 1 && instance.getObjectName() === 'MyObject')
        ).toBe(true);
        i++;
      };
      container.iterateOverInstancesWithZOrdering(functor, '');
    });
    it('moving from layers to another', function () {
      container.moveInstancesToLayer('OtherLayer', 'YetAnotherLayer');

      var functor = new gd.InitialInstanceJSFunctor();
      functor.invoke = function (instance) {
        instance = gd.wrapPointer(instance, gd.InitialInstance);
        expect(instance.getObjectName()).toBe('MyObject3');
      };
      container.iterateOverInstancesWithZOrdering(functor, 'YetAnotherLayer');
    });
    it('can be cloned', function () {
      containerCopy = container.clone();
      expect(containerCopy.getInstancesCount()).toBe(3);

      var instance = containerCopy.insertNewInitialInstance();
      instance.setObjectName('MyObject4');
      expect(containerCopy.getInstancesCount()).toBe(4);
      expect(container.getInstancesCount()).toBe(3);

      containerCopy.delete();
      containerCopy = null;
    });
    it('removing instances', function () {
      container.removeInitialInstancesOfObject('MyObject');
      expect(container.getInstancesCount()).toBe(2);
    });
    it('removing instances on a layer', function () {
      container.removeAllInstancesOnLayer('YetAnotherLayer');
      expect(container.getInstancesCount()).toBe(1);
    });
    it('can be serialized', function () {
      expect(container.serializeTo).not.toBe(undefined);
      expect(container.unserializeFrom).not.toBe(undefined);
    });

    afterAll(function () {
      container.delete();
    });
  });

  describe('gd.InitialInstance', function () {
    let project = null;
    let layout = null;
    let initialInstance = null;
    beforeAll(() => {
      project = gd.ProjectHelper.createNewGDJSProject();
      layout = project.insertNewLayout('Scene', 0);
      layout.insertNewObject(project, 'Sprite', 'MySpriteObject', 0);

      initialInstance = layout.getInitialInstances().insertNewInitialInstance();
    });

    it('properties', function () {
      initialInstance.setObjectName('MySpriteObject');
      expect(initialInstance.getObjectName()).toBe('MySpriteObject');
      initialInstance.setX(150);
      expect(initialInstance.getX()).toBe(150);
      initialInstance.setY(140);
      expect(initialInstance.getY()).toBe(140);
      initialInstance.setAngle(45);
      expect(initialInstance.getAngle()).toBe(45);
      initialInstance.setZOrder(12);
      expect(initialInstance.getZOrder()).toBe(12);
      initialInstance.setLayer('MyLayer');
      expect(initialInstance.getLayer()).toBe('MyLayer');
      initialInstance.setLocked(true);
      expect(initialInstance.isLocked()).toBe(true);
      initialInstance.setHasCustomSize(true);
      expect(initialInstance.hasCustomSize()).toBe(true);
      initialInstance.setCustomWidth(34);
      expect(initialInstance.getCustomWidth()).toBe(34);
      initialInstance.setCustomHeight(30);
      expect(initialInstance.getCustomHeight()).toBe(30);
    });
    it('Sprite object custom properties', function () {
      initialInstance.updateCustomProperty('Animation', '2', project, layout);
      expect(
        initialInstance
          .getCustomProperties(project, layout)
          .get('Animation')
          .getValue()
      ).toBe('2');
      expect(initialInstance.getRawDoubleProperty('animation')).toBe(2);
    });
    it('can be serialized', function () {
      expect(initialInstance.serializeTo).not.toBe(undefined);
      expect(initialInstance.unserializeFrom).not.toBe(undefined);

      var element = new gd.SerializerElement();
      initialInstance.serializeTo(element);

      var initialInstance2 = layout
        .getInitialInstances()
        .insertNewInitialInstance();
      initialInstance2.unserializeFrom(element);
      expect(initialInstance2.getObjectName()).toBe('MySpriteObject');
      expect(initialInstance2.getX()).toBe(150);
      expect(initialInstance2.getY()).toBe(140);
      expect(initialInstance2.getAngle()).toBe(45);
      expect(initialInstance2.getZOrder()).toBe(12);
      expect(initialInstance2.getLayer()).toBe('MyLayer');
      expect(initialInstance2.isLocked()).toBe(true);
      expect(initialInstance2.hasCustomSize()).toBe(true);
      expect(initialInstance2.getCustomWidth()).toBe(34);
      expect(initialInstance2.getCustomHeight()).toBe(30);
    });
    it('can be serialized with a persistent UUID called persistentUuid', function () {
      const initialInstance = new gd.InitialInstance();
      initialInstance.setObjectName('MyObject');

      // Serialized object must have a non empty "persistentUuid".
      var element = new gd.SerializerElement();
      initialInstance.serializeTo(element);
      const persistentUuid = element.getStringAttribute('persistentUuid');
      expect(persistentUuid).toBeTruthy();

      // The UUID must persist across serializations.
      const initialInstance2 = new gd.InitialInstance();
      initialInstance2.unserializeFrom(element);

      var element2 = new gd.SerializerElement();
      initialInstance.serializeTo(element2);
      const persistentUuid2 = element2.getStringAttribute('persistentUuid');
      expect(persistentUuid2).toBe(persistentUuid);

      // The UUID can be reset
      initialInstance2.resetPersistentUuid();
      var element3 = new gd.SerializerElement();
      initialInstance2.serializeTo(element3);
      const persistentUuid3 = element3.getStringAttribute('persistentUuid');
      expect(persistentUuid3).not.toBe(persistentUuid2);

      element.delete();
      element2.delete();
      element3.delete();
      initialInstance.delete();
      initialInstance2.delete();
    });

    afterAll(function () {
      project.delete();
    });
  });

  describe('gd.VariablesContainer', function () {
    it('container is empty after being created', function () {
      var container = new gd.VariablesContainer();

      expect(container.has('Variable')).toBe(false);
      expect(container.count()).toBe(0);
      container.delete();
    });
    it('can insert variables', function () {
      var container = new gd.VariablesContainer();

      container.insertNew('Variable', 0);
      expect(container.has('Variable')).toBe(true);
      expect(container.count()).toBe(1);

      container.insertNew('SecondVariable', 0);
      expect(container.has('SecondVariable')).toBe(true);
      expect(container.count()).toBe(2);
      container.delete();
    });
    it('can rename variables', function () {
      var container = new gd.VariablesContainer();

      container.insertNew('Variable', 0);
      container
        .insertNew('SecondVariable', 0)
        .setString('String of SecondVariable');
      container.insertNew('ThirdVariable', 0);

      expect(container.has('SecondVariable')).toBe(true);
      expect(container.has('NewName')).toBe(false);
      container.rename('SecondVariable', 'NewName');
      expect(container.has('SecondVariable')).toBe(false);
      expect(container.has('NewName')).toBe(true);

      expect(container.get('NewName').getString()).toBe(
        'String of SecondVariable'
      );
      container.delete();
    });
    it('can swap variables', function () {
      var container = new gd.VariablesContainer();

      container.insertNew('Variable', 0).setValue(4);
      container
        .insertNew('SecondVariable', 1)
        .setString('String of SecondVariable');
      container.insertNew('ThirdVariable', 2).getChild('Child1').setValue(7);

      expect(container.getNameAt(0)).toBe('Variable');
      expect(container.getNameAt(2)).toBe('ThirdVariable');

      container.swap(0, 2);
      expect(container.getNameAt(0)).toBe('ThirdVariable');
      expect(container.getNameAt(2)).toBe('Variable');
      expect(container.getAt(2).getValue()).toBe(4);

      container.delete();
    });
    it('can move variables', function () {
      var container = new gd.VariablesContainer();

      container.insertNew('Variable', 0).setValue(4);
      container
        .insertNew('SecondVariable', 1)
        .setString('String of SecondVariable');
      container.insertNew('ThirdVariable', 2).getChild('Child1').setValue(7);

      container.move(1, 2);
      expect(container.getNameAt(0)).toBe('Variable');
      expect(container.getNameAt(1)).toBe('ThirdVariable');
      expect(container.getNameAt(2)).toBe('SecondVariable');

      container.move(1, 9999);
      expect(container.getNameAt(0)).toBe('Variable');
      expect(container.getNameAt(1)).toBe('ThirdVariable');
      expect(container.getNameAt(2)).toBe('SecondVariable');

      container.move(2, 0);
      expect(container.getNameAt(0)).toBe('SecondVariable');
      expect(container.getNameAt(1)).toBe('Variable');
      expect(container.getNameAt(2)).toBe('ThirdVariable');

      container.delete();
    });
  });

  describe('gd.Variable', function () {
    let variable = null;
    beforeAll(() => (variable = new gd.Variable()));

    it('should have initial value', function () {
      expect(variable.getValue()).toBe(0);
      expect(variable.getType()).toBe(gd.Variable.Number);
    });
    it('can have a value', function () {
      variable.setValue(5);
      expect(variable.getValue()).toBe(5);
      expect(variable.getType()).toBe(gd.Variable.Number);
    });
    it('can have a string value', function () {
      variable.setString('Hello');
      expect(variable.getString()).toBe('Hello');
      expect(variable.getType()).toBe(gd.Variable.String);
    });
    it('can have a boolean value', function () {
      variable.setBool(true);
      expect(variable.getBool()).toBe(true);
      expect(variable.getType()).toBe(gd.Variable.Boolean);
    });
    it('can be a structure', function () {
      variable.getChild('FirstChild').setValue(1);
      variable.getChild('SecondChild').setString('two');
      expect(variable.hasChild('FirstChild')).toBe(true);
      expect(variable.hasChild('SecondChild')).toBe(true);
      expect(variable.hasChild('NotExisting')).toBe(false);
      expect(variable.getChild('SecondChild').getString()).toBe('two');
      expect(variable.getType()).toBe(gd.Variable.Structure);
      variable.removeChild('FirstChild');
      expect(variable.hasChild('FirstChild')).toBe(false);
      expect(variable.getType()).toBe(gd.Variable.Structure);
      variable.removeChild('SecondChild');
      expect(variable.getType()).toBe(gd.Variable.Structure);
    });
    it('can expose its children', function () {
      variable.getChild('FirstChild').setValue(1);
      variable.getChild('SecondChild').setValue(1);

      var childrenNames = variable.getAllChildrenNames();
      expect(childrenNames.size()).toBe(2);

      variable.getChild(childrenNames.get(0)).setString('one');
      variable.getChild(childrenNames.get(1)).setValue(2);

      expect(variable.getChild('FirstChild').getString()).toBe('one');
      expect(variable.getChild('SecondChild').getValue()).toBe(2);

      expect(childrenNames.size()).toBe(2);
    });
    it('can be an array', function () {
      variable.getAtIndex(0).setValue(1);
      expect(variable.getType()).toBe(gd.Variable.Array);
      variable.getAtIndex(2).setString('three');
      expect(variable.getAllChildrenArray().size()).toBe(3);
      expect(variable.getAtIndex(0).getValue()).toBe(1);
      expect(variable.getAtIndex(1).getValue()).toBe(0);
      expect(variable.getAtIndex(2).getType()).toBe(gd.Variable.String);
      variable.removeAtIndex(2);
      expect(variable.getType()).toBe(gd.Variable.Array);
      variable.removeAtIndex(1);
      variable.removeAtIndex(0);
      expect(variable.getType()).toBe(gd.Variable.Array);
    });
    it('can search inside children and remove them recursively', function () {
      var parentVariable = new gd.Variable();

      var child1 = parentVariable.getChild('Child1');
      var child2 = parentVariable.getChild('Child2');
      var grandChild1 = parentVariable.getChild('Child1').getChild('GrandChild');
      var grandChild2 = parentVariable.getChild('Child2').getAtIndex(0);

      expect(parentVariable.contains(grandChild1, true)).toBe(true);
      expect(parentVariable.contains(grandChild1, false)).toBe(false);
      expect(parentVariable.contains(grandChild2, true)).toBe(true);
      expect(parentVariable.contains(grandChild2, false)).toBe(false);
      expect(parentVariable.contains(child1, true)).toBe(true);
      expect(parentVariable.contains(child2, true)).toBe(true);
      expect(parentVariable.contains(child1, false)).toBe(true);
      expect(parentVariable.contains(child2, false)).toBe(true);

      expect(child1.contains(grandChild1, true)).toBe(true);
      expect(child1.contains(grandChild1, false)).toBe(true);
      expect(child2.contains(grandChild1, true)).toBe(false);
      expect(child2.contains(grandChild1, false)).toBe(false);

      expect(child1.contains(grandChild2, true)).toBe(false);
      expect(child1.contains(grandChild2, false)).toBe(false);
      expect(child2.contains(grandChild2, true)).toBe(true);
      expect(child2.contains(grandChild2, false)).toBe(true);

      expect(grandChild1.contains(grandChild1, true)).toBe(false);
      expect(grandChild1.contains(grandChild1, false)).toBe(false);
      expect(grandChild1.contains(child1, true)).toBe(false);
      expect(grandChild1.contains(child2, true)).toBe(false);
      expect(grandChild1.contains(parentVariable, true)).toBe(false);
      expect(grandChild1.contains(child1, false)).toBe(false);
      expect(grandChild1.contains(child2, false)).toBe(false);
      expect(grandChild1.contains(parentVariable, false)).toBe(false);

      expect(grandChild2.contains(grandChild1, true)).toBe(false);
      expect(grandChild2.contains(grandChild1, false)).toBe(false);
      expect(grandChild2.contains(child1, true)).toBe(false);
      expect(grandChild2.contains(child2, true)).toBe(false);
      expect(grandChild2.contains(parentVariable, true)).toBe(false);
      expect(grandChild2.contains(child1, false)).toBe(false);
      expect(grandChild2.contains(child2, false)).toBe(false);
      expect(grandChild2.contains(parentVariable, false)).toBe(false);

      parentVariable.removeRecursively(grandChild1);
      expect(child1.hasChild('GrandChild')).toBe(false);
      expect(child1.getChildrenCount()).toBe(0);
      expect(parentVariable.getChildrenCount()).toBe(2);

      parentVariable.removeRecursively(grandChild2);
      expect(child2.getAllChildrenArray().size()).toBe(0);
      expect(child2.getChildrenCount()).toBe(0);
      expect(parentVariable.getChildrenCount()).toBe(2);

      parentVariable.removeRecursively(child2);
      expect(parentVariable.getChildrenCount()).toBe(1);
      expect(parentVariable.hasChild('Child1')).toBe(true);
      expect(parentVariable.hasChild('Child2')).toBe(false);

      parentVariable.delete();
    });

    afterAll(function () {
      variable.delete();
    });
  });

  describe('gd.ImageResource', function () {
    it('should have name and file', function () {
      const resource = new gd.ImageResource();
      resource.setName('MyResource');
      resource.setFile('MyFile');
      expect(resource.getName()).toBe('MyResource');
      expect(resource.getFile()).toBe('MyFile');
      resource.delete();
    });
    it('can have metadata', function () {
      const resource = new gd.ImageResource();
      expect(resource.getMetadata()).toBe('');
      resource.setMetadata(JSON.stringify({ hello: 'world' }));
      expect(resource.getMetadata()).toBe('{"hello":"world"}');
      resource.delete();
    });

    it('has smooth and alreadyLoaded custom properties', function () {
      const project = gd.ProjectHelper.createNewGDJSProject();
      const resource = new gd.ImageResource();

      const properties = resource.getProperties();
      expect(properties.get('Smooth the image').getValue()).toBe('true');
      expect(properties.get('Always loaded in memory').getValue()).toBe(
        'false'
      );

      // Note: updateProperty expect the booleans in an usual "0" or "1" format.
      resource.updateProperty('Smooth the image', '0');
      resource.updateProperty('Always loaded in memory', '1');

      const updatedProperties = resource.getProperties();
      expect(updatedProperties.get('Smooth the image').getValue()).toBe(
        'false'
      );
      expect(updatedProperties.get('Always loaded in memory').getValue()).toBe(
        'true'
      );

      resource.delete();
      project.delete();
    });
  });

  describe('gd.AudioResource', function () {
    it('should have name and file', function () {
      const resource = new gd.AudioResource();
      resource.setName('MyAudioResource');
      resource.setFile('MyAudioFile');
      expect(resource.getName()).toBe('MyAudioResource');
      expect(resource.getFile()).toBe('MyAudioFile');
      resource.delete();
    });
    it('can have metadata', function () {
      const resource = new gd.AudioResource();
      expect(resource.getMetadata()).toBe('');
      resource.setMetadata(JSON.stringify({ hello: 'world' }));
      expect(resource.getMetadata()).toBe('{"hello":"world"}');
      resource.delete();
    });
  });

  describe('gd.FontResource', function () {
    it('should have name and file', function () {
      const resource = new gd.FontResource();
      resource.setName('MyFontResource');
      resource.setFile('MyFontFile');
      expect(resource.getName()).toBe('MyFontResource');
      expect(resource.getFile()).toBe('MyFontFile');
      resource.delete();
    });
    it('can have metadata', function () {
      const resource = new gd.FontResource();
      expect(resource.getMetadata()).toBe('');
      resource.setMetadata(JSON.stringify({ hello: 'world' }));
      expect(resource.getMetadata()).toBe('{"hello":"world"}');
      resource.delete();
    });
  });

  describe('gd.VideoResource', function () {
    it('should have name and file', function () {
      const resource = new gd.VideoResource();
      resource.setName('MyVideoResource');
      resource.setFile('MyVideoFile');
      expect(resource.getName()).toBe('MyVideoResource');
      expect(resource.getFile()).toBe('MyVideoFile');
      resource.delete();
    });
    it('can have metadata', function () {
      const resource = new gd.VideoResource();
      expect(resource.getMetadata()).toBe('');
      resource.setMetadata(JSON.stringify({ hello: 'world' }));
      expect(resource.getMetadata()).toBe('{"hello":"world"}');
      resource.delete();
    });
  });

  describe('gd.JsonResource', function () {
    it('should have name and file', function () {
      const resource = new gd.JsonResource();
      resource.setName('MyJsonResource');
      resource.setFile('MyJsonFile');
      expect(resource.getName()).toBe('MyJsonResource');
      expect(resource.getFile()).toBe('MyJsonFile');
      resource.delete();
    });
    it('can have metadata', function () {
      const resource = new gd.JsonResource();
      expect(resource.getMetadata()).toBe('');
      resource.setMetadata(JSON.stringify({ hello: 'world' }));
      expect(resource.getMetadata()).toBe('{"hello":"world"}');
      resource.delete();
    });

    it('has disablePreload custom properties', function () {
      const project = gd.ProjectHelper.createNewGDJSProject();
      const resource = new gd.JsonResource();

      const properties = resource.getProperties();
      expect(properties.get('disablePreload').getValue()).toBe('false');

      // Note: updateProperty expect the booleans in an usual "0" or "1" format.
      resource.updateProperty('disablePreload', '1');

      const updatedProperties = resource.getProperties();
      expect(updatedProperties.get('disablePreload').getValue()).toBe('true');

      resource.delete();
      project.delete();
    });
  });

  describe('gd.ResourcesManager', function () {
    it('should support adding resources', function () {
      var project = gd.ProjectHelper.createNewGDJSProject();
      var resource = new gd.Resource();
      var resource2 = new gd.Resource();
      resource.setName('MyResource');
      resource2.setName('MyResource2');
      project.getResourcesManager().addResource(resource);
      project.getResourcesManager().addResource(resource2);
      var allResources = project.getResourcesManager().getAllResourceNames();

      expect(allResources.size()).toBe(2);
      project.delete();
    });

    it('should support finding resources', function () {
      var project = gd.ProjectHelper.createNewGDJSProject();
      var resource = new gd.Resource();
      var resource2 = new gd.Resource();
      resource.setName('MyResource');
      resource2.setName('MyResource2');
      project.getResourcesManager().addResource(resource);
      project.getResourcesManager().addResource(resource2);

      expect(
        project.getResourcesManager().getResourcePosition('MyResource')
      ).toBe(0);
      expect(
        project.getResourcesManager().getResourcePosition('MyResource2')
      ).toBe(1);
      expect(
        project.getResourcesManager().getResourcePosition('MyResource3')
      ).toBe(-1);
      project.delete();
    });

    it('should support removing resources', function () {
      var project = gd.ProjectHelper.createNewGDJSProject();
      var resource = new gd.Resource();
      resource.setName('MyResource');
      project.getResourcesManager().addResource(resource);
      project.getResourcesManager().removeResource('MyResource');

      var allResources = project.getResourcesManager().getAllResourceNames();
      expect(allResources.size()).toBe(0);
      project.delete();
    });
  });

  describe('gd.ProjectResourcesAdder', function () {
    it('should support removing useless resources', function () {
      var project = gd.ProjectHelper.createNewGDJSProject();
      var resource1 = new gd.ImageResource();
      resource1.setName('Useless');
      var resource2 = new gd.ImageResource();
      resource2.setName('Used');
      project.getResourcesManager().addResource(resource1);
      project.getResourcesManager().addResource(resource2);

      //Create an object using a resource
      var obj = project.insertNewObject(project, 'Sprite', 'MyObject', 0);
      var sprite1 = new gd.Sprite();
      sprite1.setImageName('Used');

      var anim1 = new gd.Animation();
      anim1.setDirectionsCount(1);
      anim1.getDirection(0).addSprite(sprite1);

      gd.castObject(obj, gd.SpriteObject).addAnimation(anim1);

      var allResources = project.getResourcesManager().getAllResourceNames();
      expect(allResources.size()).toBe(2);

      gd.ProjectResourcesAdder.removeAllUseless(project, 'image');

      var allResources = project.getResourcesManager().getAllResourceNames();
      expect(allResources.size()).toBe(1);
      expect(allResources.get(0)).toBe('Used');
      project.delete();
    });
  });

  describe('gd.ArbitraryResourceWorker', function () {
    it('should be called with resources of the project', function (done) {
      var project = gd.ProjectHelper.createNewGDJSProject();
      var obj = project.insertNewObject(project, 'Sprite', 'MyObject', 0);
      const spriteObject = gd.asSpriteObject(obj);
      var sprite1 = new gd.Sprite();
      sprite1.setImageName('Used');
      const animation = new gd.Animation();
      animation.setDirectionsCount(1);
      animation.getDirection(0).addSprite(sprite1);
      spriteObject.addAnimation(animation);

      var worker = extend(new gd.ArbitraryResourceWorkerJS(), {
        exposeImage: function (image) {
          expect(image).toBe('Used');
          done();

          return image;
        },
      });

      project.exposeResources(worker);
      project.delete();
    });
  });

  describe('gd.ResourcesInUseHelper', function () {
    it('should find the images used by objects', function () {
      var sprite1 = new gd.Sprite();
      sprite1.setImageName('Image1');
      var sprite2 = new gd.Sprite();
      sprite2.setImageName('Image2');
      var sprite3 = new gd.Sprite();
      sprite3.setImageName('Image3');

      const spriteObject = new gd.SpriteObject('My sprite object');
      const animation = new gd.Animation();
      animation.setDirectionsCount(1);
      animation.getDirection(0).addSprite(sprite1);
      animation.getDirection(0).addSprite(sprite2);
      animation.getDirection(0).addSprite(sprite1);
      spriteObject.addAnimation(animation);

      const spriteObject2 = new gd.SpriteObject('My sprite object');
      const animation2 = new gd.Animation();
      animation2.setDirectionsCount(1);
      animation2.getDirection(0).addSprite(sprite1);
      animation2.getDirection(0).addSprite(sprite3);
      animation2.getDirection(0).addSprite(sprite1);
      spriteObject2.addAnimation(animation2);

      const resourcesInUse = new gd.ResourcesInUseHelper();

      {
        spriteObject.exposeResources(resourcesInUse);
        const resourceNames = resourcesInUse
          .getAllImages()
          .toNewVectorString()
          .toJSArray();
        expect(resourceNames).toHaveLength(2);
        expect(resourceNames).toContain('Image1');
        expect(resourceNames).toContain('Image2');
      }

      {
        spriteObject2.exposeResources(resourcesInUse);
        const resourceNames = resourcesInUse
          .getAllImages()
          .toNewVectorString()
          .toJSArray();
        expect(resourceNames).toHaveLength(3);
        expect(resourceNames).toContain('Image1');
        expect(resourceNames).toContain('Image2');
        expect(resourceNames).toContain('Image3');
      }

      resourcesInUse.delete();

      spriteObject.delete();
      spriteObject2.delete();
    });
  });

  describe('gd.Behavior', function () {
    it('update a not existing property', function () {
      const project = gd.ProjectHelper.createNewGDJSProject();
      const behavior = new gd.Behavior();
      const serializerElement = new gd.SerializerElement();

      expect(
        behavior.updateProperty(
          serializerElement,
          'PropertyThatDoesNotExist',
          'MyValue'
        )
      ).toBe(false);

      serializerElement.delete();
      behavior.delete();
      project.delete();
    });
  });

  describe('gd.BehaviorsSharedData', function () {
    it('can be created by gd.Layout.updateBehaviorsSharedData', function () {
      var project = gd.ProjectHelper.createNewGDJSProject();
      var layout = project.insertNewLayout('Scene', 0);
      var object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);

      layout.updateBehaviorsSharedData(project);
      expect(layout.hasBehaviorSharedData('Physics')).toBe(false);
      var behaviorContent = object.addNewBehavior(
        project,
        'PhysicsBehavior::PhysicsBehavior',
        'Physics'
      );
      expect(layout.hasBehaviorSharedData('Physics')).toBe(false);
      layout.updateBehaviorsSharedData(project);
      expect(layout.hasBehaviorSharedData('Physics')).toBe(true);
      layout.removeObject('MyObject');
      expect(layout.hasBehaviorSharedData('Physics')).toBe(true);
      layout.updateBehaviorsSharedData(project);
      expect(layout.hasBehaviorSharedData('Physics')).toBe(false);

      project.delete();
    });
  });

  describe('gd.BehaviorSharedDataJsImplementation', function () {
    it('can declare a gd.BehaviorSharedDataJsImplementation and pass sanity checks', function () {
      var mySharedData = new gd.BehaviorSharedDataJsImplementation();
      mySharedData.updateProperty = function (
        behaviorContent,
        propertyName,
        newValue
      ) {
        if (propertyName === 'My first property') {
          behaviorContent.setStringAttribute('property1', newValue);
          return true;
        }
        if (propertyName === 'My other property') {
          behaviorContent.setBoolAttribute('property2', newValue === '1');
          return true;
        }

        return false;
      };
      mySharedData.getProperties = function (behaviorContent) {
        var properties = new gd.MapStringPropertyDescriptor();

        properties
          .getOrCreate('My first property')
          .setValue(behaviorContent.getStringAttribute('property1'));
        properties
          .getOrCreate('My other property')
          .setValue(behaviorContent.getBoolAttribute('property2') ? '1' : '0')
          .setType('Boolean');

        return properties;
      };
      mySharedData.initializeContent = function (behaviorContent) {
        behaviorContent.setStringAttribute('property1', 'Initial value 1');
        behaviorContent.setBoolAttribute('property2', true);
      };

      try {
        expect(
          gd.ProjectHelper.sanityCheckBehaviorsSharedDataProperty(
            mySharedData,
            'My first property',
            'Value1'
          )
        ).toBe('');
        expect(
          gd.ProjectHelper.sanityCheckBehaviorsSharedDataProperty(
            mySharedData,
            'My other property',
            '0'
          )
        ).toBe('');
      } catch (ex) {
        console.error(ex);

        throw Error(
          'Exception caught while launching sanityCheckBehavior on a gd.BehaviorSharedDataJsImplementation.'
        );
      }
    });
  });

  describe('gd.NamedPropertyDescriptor', function () {
    const makeNewProperty = () => {
      const property = new gd.NamedPropertyDescriptor();
      property
        .setName('Property1')
        .setLabel('The first property')
        .setValue('Hello world')
        .setType('string')
        .addExtraInfo('Info1')
        .addExtraInfo('Info2');

      return property;
    };

    it('can be created and manipulated', function () {
      const property = makeNewProperty();
      expect(property.getName()).toBe('Property1');
      expect(property.getLabel()).toBe('The first property');
      expect(property.getValue()).toBe('Hello world');
      expect(property.getType()).toBe('string');
      expect(property.getExtraInfo().toJSArray()).toContain('Info1');
      expect(property.getExtraInfo().toJSArray()).toContain('Info2');

      property.delete();
    });
    it('can be serialized', function () {
      const property = makeNewProperty();

      var serializerElement = new gd.SerializerElement();
      property.serializeTo(serializerElement);
      property.delete();

      const property2 = new gd.NamedPropertyDescriptor();
      property2.unserializeFrom(serializerElement);
      serializerElement.delete();

      expect(property2.getName()).toBe('Property1');
      expect(property2.getLabel()).toBe('The first property');
      expect(property2.getValue()).toBe('Hello world');
      expect(property2.getType()).toBe('string');
      expect(property2.getExtraInfo().toJSArray()).toContain('Info1');
      expect(property2.getExtraInfo().toJSArray()).toContain('Info2');
    });
  });

  describe('gd.NamedPropertyDescriptorsList', function () {
    it('can be used to store named properties', function () {
      const list = new gd.NamedPropertyDescriptorsList();

      const property1 = list.insertNew('Property1', 0);
      expect(list.has('Property1')).toBe(true);
      expect(list.getCount()).toBe(1);

      expect(property1.getName()).toBe('Property1');
      expect(list.getAt(0).getName()).toBe('Property1');

      property1.setLabel('Property 1');
      property1.setValue('123');
      expect(list.getAt(0).getLabel()).toBe('Property 1');
      expect(list.getAt(0).getValue()).toBe('123');
      list.delete();
    });
  });

  describe('gd.MapStringPropertyDescriptor', function () {
    it('can be used to manipulate properties', function () {
      var properties = new gd.MapStringPropertyDescriptor();
      expect(properties.has('Property0')).toBe(false);

      // Ensure the "set" method works (though in practice, prefer "getOrCreate")
      const property1 = new gd.PropertyDescriptor('Hello Property1');
      properties.set('Property1', property1);
      property1.delete();

      expect(properties.get('Property1').getValue()).toBe('Hello Property1');
      expect(properties.get('Property1').getType()).toBe('string');
      properties.get('Property1').setValue('Hello modified Property1');
      expect(properties.get('Property1').getValue()).toBe(
        'Hello modified Property1'
      );
      expect(properties.keys().toJSArray()).not.toContain('Property0');
      expect(properties.keys().toJSArray()).toContain('Property1');

      // Ensure the "getOrCreate" method works
      expect(properties.has('Property0')).toBe(false);
      properties
        .getOrCreate('Property0')
        .setValue('Hello Property0')
        .setType('another type')
        .addExtraInfo('Info1')
        .addExtraInfo('Info3');
      expect(properties.has('Property0')).toBe(true);
      expect(properties.get('Property0').getValue()).toBe('Hello Property0');
      expect(properties.get('Property0').getType()).toBe('another type');
      expect(properties.get('Property0').getExtraInfo().toJSArray()).toContain(
        'Info1'
      );
      expect(
        properties.get('Property0').getExtraInfo().toJSArray()
      ).not.toContain('Info2');
      expect(properties.get('Property0').getExtraInfo().toJSArray()).toContain(
        'Info3'
      );
      expect(properties.has('Property0')).toBe(true);
      expect(properties.has('Property1')).toBe(true);
      expect(properties.keys().toJSArray()).toContain('Property0');
      expect(properties.keys().toJSArray()).toContain('Property1');
      properties.delete();
    });
  });

  describe('gd.BehaviorJsImplementation', function () {
    it('can declare a gd.BehaviorJsImplementation and pass sanity checks', function () {
      var myBehavior = new gd.BehaviorJsImplementation();
      myBehavior.updateProperty = function (
        behaviorContent,
        propertyName,
        newValue
      ) {
        if (propertyName === 'My first property') {
          behaviorContent.setStringAttribute('property1', newValue);
          return true;
        }
        if (propertyName === 'My other property') {
          behaviorContent.setBoolAttribute('property2', newValue === '1');
          return true;
        }

        return false;
      };
      myBehavior.getProperties = function (behaviorContent) {
        var properties = new gd.MapStringPropertyDescriptor();

        properties
          .getOrCreate('My first property')
          .setValue(behaviorContent.getStringAttribute('property1'));
        properties
          .getOrCreate('My other property')
          .setValue(behaviorContent.getBoolAttribute('property2') ? '1' : '0')
          .setType('Boolean');

        return properties;
      };
      myBehavior.initializeContent = function (behaviorContent) {
        behaviorContent.setStringAttribute('property1', 'Initial value 1');
        behaviorContent.setBoolAttribute('property2', true);
      };

      try {
        expect(
          gd.ProjectHelper.sanityCheckBehaviorProperty(
            myBehavior,
            'My first property',
            'Value1'
          )
        ).toBe('');
        expect(
          gd.ProjectHelper.sanityCheckBehaviorProperty(
            myBehavior,
            'My other property',
            '0'
          )
        ).toBe('');
      } catch (ex) {
        console.error(ex);

        throw Error(
          'Exception caught while launching sanityCheckBehavior on a gd.BehaviorJsImplementation.'
        );
      }
    });
  });

  describe('gd.Object', function () {
    var project = null;
    var layout = null;
    var object = null;
    var object2 = null;

    beforeAll(() => {
      project = gd.ProjectHelper.createNewGDJSProject();
      layout = project.insertNewLayout('Scene', 0);
      object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);
      object2 = layout.insertNewObject(project, 'Sprite', 'MyObject2', 1);
    });

    it('has properties and initial values', function () {
      object.setName('TheObject');
      expect(object.getName()).toBe('TheObject');
      expect(object.hasBehaviorNamed('DoNotExists')).toBe(false);
    });

    it('can have its type retrieved with gd.getTypeOfObject', function () {
      expect(gd.getTypeOfObject(project, layout, 'TheObject', true)).toBe(
        'Sprite'
      );
    });

    it('can have behaviors', function () {
      var behavior = object.addNewBehavior(
        project,
        'DraggableBehavior::Draggable',
        'Draggable'
      );
      expect(object.hasBehaviorNamed('Draggable')).toBe(true);
      expect(object.getBehavior('Draggable')).toBe(behavior);
    });

    it('can have its behaviors retrieved with gd.getBehaviorsOfObject', function () {
      var behaviors = gd.getBehaviorsOfObject(
        project,
        layout,
        'TheObject',
        true
      );
      expect(behaviors.size()).toBe(1);
      expect(behaviors.get(0)).toBe('Draggable');
    });

    it('can be un/serialized (basic)', function () {
      var serializerElement = new gd.SerializerElement();
      object.serializeTo(serializerElement);
      object2.unserializeFrom(project, serializerElement);
      object2.unserializeFrom(project, serializerElement); // Also check that multiple
      object2.unserializeFrom(project, serializerElement); // unserialization is idempotent
      serializerElement.delete();

      //Check that behaviors were persisted and restored
      var behaviors = object2.getAllBehaviorNames();
      expect(behaviors.size()).toBe(1);
      expect(behaviors.at(0)).toBe('Draggable');
    });

    it('can be un/serialized (with behavior content)', function () {
      const behaviorContent = object.getBehavior('Draggable');
      behaviorContent.getContent().addChild('Child1');
      behaviorContent
        .getContent()
        .addChild('Child2')
        .setStringValue('Child2Value');

      var serializerElement = new gd.SerializerElement();
      object.serializeTo(serializerElement);
      object2.unserializeFrom(project, serializerElement);
      object2.unserializeFrom(project, serializerElement); // Also check that multiple
      object2.unserializeFrom(project, serializerElement); // unserialization is idempotent
      serializerElement.delete();

      //Check that behaviors were persisted and restored
      var behaviors = object2.getAllBehaviorNames();
      expect(behaviors.size()).toBe(1);
      expect(behaviors.at(0)).toBe('Draggable');

      const behaviorContent2 = object2.getBehavior('Draggable');
      expect(behaviorContent2.getContent().hasChild('Child1')).toBe(true);
      expect(behaviorContent2.getContent().hasChild('Child2')).toBe(true);
      expect(
        behaviorContent2.getContent().getChild('Child2').getStringValue()
      ).toBe('Child2Value');
    });

    afterAll(function () {
      project.delete();
    });
  });

  describe('gd.ObjectJsImplementation', function () {
    const createSampleObjectJsImplementation = () => {
      var myObject = new gd.ObjectJsImplementation();
      myObject.updateProperty = function (content, propertyName, newValue) {
        if (propertyName === 'My first property') {
          content.property1 = newValue;
          return true;
        }
        if (propertyName === 'My other property') {
          content.property2 = newValue === '1';
          return true;
        }

        return false;
      };
      myObject.getProperties = function (content) {
        var properties = new gd.MapStringPropertyDescriptor();

        properties.getOrCreate('My first property').setValue(content.property1);
        properties
          .getOrCreate('My other property')
          .setValue(content.property2 ? '1' : '0')
          .setType('Boolean');

        return properties;
      };
      myObject.setRawJSONContent(
        JSON.stringify({
          property1: 'Initial value 1',
          property2: true,
        })
      );

      myObject.updateInitialInstanceProperty = function (
        content,
        instance,
        propertyName,
        newValue,
        project,
        layout
      ) {
        if (propertyName === 'My instance property') {
          instance.setRawStringProperty('instanceprop1', newValue);
          return true;
        }
        if (propertyName === 'My other instance property') {
          instance.setRawDoubleProperty('instanceprop2', parseFloat(newValue));
          return true;
        }

        return false;
      };
      myObject.getInitialInstanceProperties = function (
        content,
        instance,
        project,
        layout
      ) {
        var properties = new gd.MapStringPropertyDescriptor();

        properties
          .getOrCreate('My instance property')
          .setValue(instance.getRawStringProperty('instanceprop1'));
        properties
          .getOrCreate('My other instance property')
          .setValue(instance.getRawDoubleProperty('instanceprop2').toString())
          .setType('number');

        return properties;
      };

      // TODO: Workaround a bad design of ObjectJsImplementation. When getProperties
      // and associated methods are redefined in JS, they have different arguments (
      // see ObjectJsImplementation C++ implementation). If called directly here from JS,
      // the arguments will be mismatched. To workaround this, always case the object to
      // a base gdObject to ensure C++ methods are called.
      return gd.castObject(myObject, gd.Object);
    };

    it('can create a gd.ObjectJsImplementation and pass sanity checks', function () {
      const myObject = createSampleObjectJsImplementation();

      try {
        expect(
          gd.ProjectHelper.sanityCheckObjectProperty(
            myObject,
            'My first property',
            'Value1'
          )
        ).toBe('');
        expect(
          gd.ProjectHelper.sanityCheckObjectProperty(
            myObject,
            'My other property',
            '0'
          )
        ).toBe('');
        expect(
          gd.ProjectHelper.sanityCheckObjectInitialInstanceProperty(
            myObject,
            'My instance property',
            'Value1'
          )
        ).toBe('');
        expect(
          gd.ProjectHelper.sanityCheckObjectInitialInstanceProperty(
            myObject,
            'My other instance property',
            '0'
          )
        ).toBe('');
      } catch (ex) {
        console.error(ex);

        throw Error(
          'Exception caught while launching sanityCheckObject on a gd.ObjectJsImplementation.'
        );
      }
    });

    it('can clone a gd.ObjectJsImplementation', function () {
      const project = gd.ProjectHelper.createNewGDJSProject();
      const object1 = createSampleObjectJsImplementation();
      expect(
        object1.getProperties().get('My first property').getValue() ==
          'Initial value 1'
      );

      object1.updateProperty('My first property', 'test1');
      const object2 = object1.clone().release();
      const object3 = object1.clone().release();

      {
        const propertiesObject1 = object1.getProperties();
        expect(propertiesObject1.has('My first property'));
        expect(
          propertiesObject1.get('My first property').getValue() == 'test1'
        );
        const propertiesObject2 = object2.getProperties();
        expect(propertiesObject2.has('My first property'));
        expect(
          propertiesObject2.get('My first property').getValue() == 'test1'
        );
      }

      {
        object1.updateProperty('My first property', 'updated value');
        const propertiesObject1 = object1.getProperties();
        expect(propertiesObject1.has('My first property'));
        expect(
          propertiesObject1.get('My first property').getValue() ==
            'updated value'
        );
        const propertiesObject2 = object2.getProperties();
        expect(propertiesObject2.has('My first property'));
        expect(
          propertiesObject2.get('My first property').getValue() == 'test1'
        );
      }

      {
        object2.updateProperty('My first property', 'updated value object 2');
        const propertiesObject1 = object1.getProperties();
        expect(propertiesObject1.has('My first property'));
        expect(
          propertiesObject1.get('My first property').getValue() ==
            'updated value'
        );
        const propertiesObject2 = object2.getProperties();
        expect(propertiesObject2.has('My first property'));
        expect(
          propertiesObject2.get('My first property').getValue() ==
            'updated value object 2'
        );
        const propertiesObject3 = object3.getProperties();
        expect(propertiesObject3.has('My first property'));
        expect(
          propertiesObject3.get('My first property').getValue() == 'test1'
        );
      }

      project.delete();
    });
  });

  describe('gd.ObjectGroupsContainer', function () {
    var container = null;
    var group1 = null;
    var group2 = null;
    var group3 = null;

    beforeAll(() => (container = new gd.ObjectGroupsContainer()));

    it('can have groups inserted', function () {
      group1 = container.insertNew('Group1', 0);
      group2 = container.insertNew('Group2', 1);
      group3 = container.insertNew('Group3', 2);

      expect(container.getAt(0).getName()).toBe('Group1');
      expect(container.getAt(1).getName()).toBe('Group2');
      expect(container.getAt(2).getName()).toBe('Group3');
      expect(container.has('Group1')).toBe(true);
      expect(container.has('Group2')).toBe(true);
      expect(container.has('Group3')).toBe(true);
      expect(container.has('Group4')).toBe(false);
      expect(container.get('Group1').getName()).toBe('Group1');
      expect(container.get('Group2').getName()).toBe('Group2');
      expect(container.get('Group3').getName()).toBe('Group3');
      expect(container.count()).toBe(3);
    });

    it('can move groups', function () {
      container.move(0, 1);
      expect(container.getAt(0).getName()).toBe('Group2');
      expect(container.getAt(1).getName()).toBe('Group1');
      expect(container.getAt(2).getName()).toBe('Group3');
    });

    it('can rename groups', function () {
      container.rename('Inexisting', 'Whatever');
      container.rename('Group1', 'Group1Renamed');

      expect(container.has('Group1')).toBe(false);
      expect(container.has('Group1Renamed')).toBe(true);
    });

    it('can remove groups', function () {
      container.remove('Group2');
      expect(container.has('Group2')).toBe(false);
      expect(container.has('Group3')).toBe(true);
      expect(container.count()).toBe(2);
    });
  });

  describe('gd.Instruction', function () {
    it('initial values', function () {
      var instr = new gd.Instruction();
      expect(instr.getParametersCount()).toBe(0);
      expect(instr.getSubInstructions().size()).toBe(0);
      instr.delete();
    });
    it('setting parameters', function () {
      var instr = new gd.Instruction();
      instr.setParametersCount(3);
      expect(instr.getParametersCount()).toBe(3);
      expect(instr.getParameter(1)).toBe('');
      instr.setParameter(2, 'MyValue');
      expect(instr.getParameter(2)).toBe('MyValue');
      instr.delete();
    });
    it('can be cloned', function () {
      var instr = new gd.Instruction();
      instr.setParametersCount(3);
      instr.setParameter(2, 'MyValue');

      var newInstr = instr.clone();
      expect(newInstr.getParametersCount()).toBe(3);
      expect(newInstr.getParameter(1)).toBe('');
      expect(newInstr.getParameter(2)).toBe('MyValue');

      newInstr.setParameter(2, 'MyChangedValue');
      expect(instr.getParameter(2)).toBe('MyValue');
      expect(newInstr.getParameter(2)).toBe('MyChangedValue');
      newInstr.delete();
      expect(instr.getParameter(2)).toBe('MyValue');

      instr.delete();
    });
  });

  describe('gd.InstructionsList', function () {
    var list = null;
    beforeAll(() => (list = new gd.InstructionsList()));

    it('can insert instructions', function () {
      expect(list.size()).toBe(0);
      list.insert(new gd.Instruction(), 0);
      expect(list.size()).toBe(1);
    });
    it('can modify its instructions', function () {
      expect(list.get(0).getType()).toBe('');

      var newInstr = new gd.Instruction();
      newInstr.setType('Type2');
      list.set(0, newInstr);

      expect(list.get(0).getType()).toBe('Type2');
      expect(list.size()).toBe(1);
    });
    it('can remove its instructions', function () {
      var newInstr = new gd.Instruction();
      newInstr.setType('Type3');
      var instruction = list.insert(newInstr, 1);
      expect(list.get(1).getType()).toBe('Type3');
      expect(list.size()).toBe(2);
      expect(list.contains(instruction)).toBe(true);

      list.remove(instruction);
      expect(list.size()).toBe(1);
      expect(list.get(0).getType()).toBe('Type2');
    });
    it('can clear its instructions', function () {
      list.clear();
      expect(list.size()).toBe(0);
    });
    it('can insert events from another list', function () {
      var list1 = new gd.InstructionsList();
      var list2 = new gd.InstructionsList();

      var newInstr = new gd.Instruction();
      newInstr.setType('Type1');
      list1.insert(newInstr, 0);
      var newInstr2 = new gd.Instruction();
      newInstr2.setType('Type2');
      list1.insert(newInstr2, 1);

      list2.insertInstructions(list1, 0, list1.size(), 0);
      expect(list2.size()).toBe(2);
      expect(list2.get(0).getType()).toBe('Type1');
      expect(list2.get(1).getType()).toBe('Type2');

      list2.insertInstructions(list1, 0, list1.size(), 1);
      expect(list2.size()).toBe(4);
      expect(list2.get(0).getType()).toBe('Type1');
      expect(list2.get(1).getType()).toBe('Type1');
      expect(list2.get(2).getType()).toBe('Type2');
      expect(list2.get(3).getType()).toBe('Type2');
      list1.delete();
      list2.delete();
    });
    it('can be un/serialized', function () {
      var newInstr = new gd.Instruction();
      newInstr.setType('Type1');
      newInstr.setParametersCount(2);
      newInstr.setParameter(0, 'Param1');
      newInstr.setParameter(1, 'Param2');
      var instruction = list.insert(newInstr, 1);

      var newInstr2 = new gd.Instruction();
      newInstr2.setType('Type2');
      newInstr2.setParametersCount(1);
      newInstr2.setParameter(0, 'Param3');
      var instruction2 = list.insert(newInstr2, 1);

      var project = gd.ProjectHelper.createNewGDJSProject();
      var serializerElement = new gd.SerializerElement();
      list.serializeTo(serializerElement);

      var list2 = new gd.InstructionsList();
      list2.unserializeFrom(project, serializerElement);

      expect(list2.size()).toBe(2);
      expect(list2.get(0).getType()).toBe('Type1');
      expect(list2.get(1).getType()).toBe('Type2');
      expect(list2.get(0).getParametersCount()).toBe(2);
      expect(list2.get(1).getParametersCount()).toBe(1);
      expect(list2.get(0).getParameter(0)).toBe('Param1');
      expect(list2.get(0).getParameter(1)).toBe('Param2');
      expect(list2.get(1).getParameter(0)).toBe('Param3');

      list2.delete();
      project.delete();
    });

    afterAll(function () {
      list.delete();
    });
  });

  describe('InstructionSentenceFormatter', function () {
    it('should translate instructions (plain text or into a vector of text with formatting)', function () {
      var action = new gd.Instruction(); //Create a simple instruction
      action.setType('Delete');
      action.setParametersCount(2);
      action.setParameter(0, 'MyCharacter');

      var actionSentenceInEnglish = gd.InstructionSentenceFormatter.get().translate(
        action,
        gd.MetadataProvider.getActionMetadata(gd.JsPlatform.get(), 'Delete')
      );
      expect(actionSentenceInEnglish).toBe('Delete MyCharacter');

      var formattedTexts = gd.InstructionSentenceFormatter.get().getAsFormattedText(
        action,
        gd.MetadataProvider.getActionMetadata(gd.JsPlatform.get(), 'Delete')
      );

      expect(formattedTexts.size()).toBe(2);
      expect(formattedTexts.getString(0)).toBe('Delete ');
      expect(formattedTexts.getString(1)).toBe('MyCharacter');
      expect(formattedTexts.getTextFormatting(0).getUserData()).not.toBe(0);
      expect(formattedTexts.getTextFormatting(1).getUserData()).toBe(0);

      action.delete();
    });
  });

  describe('gd.EventsList', function () {
    it('can have events', function () {
      var list = new gd.EventsList();
      list.insertEvent(new gd.StandardEvent(), 0);
      var lastEvent = list.insertEvent(new gd.StandardEvent(), 1);
      list.insertEvent(new gd.StandardEvent(), 1);
      expect(list.getEventsCount()).toBe(3);
      expect(list.getEventAt(2).ptr).toBe(lastEvent.ptr);
      list.delete();
    });

    it('can create lots of new events', function () {
      var project = new gd.ProjectHelper.createNewGDJSProject();
      var list = new gd.EventsList();
      for (var i = 0; i < 500; ++i) {
        var evt = list.insertNewEvent(
          project,
          'BuiltinCommonInstructions::Standard',
          0
        );
        expect(evt.getType()).toBe('BuiltinCommonInstructions::Standard');
        expect(gd.asStandardEvent(list.getEventAt(0)).getType()).toBe(
          'BuiltinCommonInstructions::Standard'
        );
        expect(list.getEventAt(0).getType()).toBe(
          'BuiltinCommonInstructions::Standard'
        );
      }
      project.delete();
      list.delete();
    });

    it('can tell if it contains an event', function () {
      var list = new gd.EventsList();

      var parentEvent = list.insertEvent(new gd.StandardEvent(), 0);
      var subEvent = parentEvent
        .getSubEvents()
        .insertEvent(new gd.StandardEvent(), 0);

      expect(list.contains(parentEvent, false)).toBe(true);
      expect(list.contains(subEvent, false)).toBe(false);
      expect(list.contains(subEvent, true)).toBe(true);

      list.delete();
    });

    it('can move an event to another list without invalidating it/copying it in memory', function () {
      var list1 = new gd.EventsList();
      var list2 = new gd.EventsList();

      var list1ParentEvent = list1.insertEvent(new gd.StandardEvent(), 0);
      var list2ParentEvent = list2.insertEvent(new gd.StandardEvent(), 0);
      var originalSubEvent = list1ParentEvent
        .getSubEvents()
        .insertEvent(new gd.StandardEvent(), 0);
      var originalSubEventPtr = originalSubEvent.ptr;

      expect(
        list1ParentEvent
          .getSubEvents()
          .moveEventToAnotherEventsList(
            originalSubEvent,
            list2ParentEvent.getSubEvents(),
            0
          )
      ).toBe(true);
      expect(list2ParentEvent.getSubEvents().getEventsCount()).toBe(1);
      var movedSubEvent = list2ParentEvent.getSubEvents().getEventAt(0);
      expect(movedSubEvent.ptr).toBe(originalSubEventPtr);

      list1.delete();
      list2.delete();
    });
  });

  describe('gd.BaseEvent', function () {
    it('can have a type', function () {
      var event = new gd.BaseEvent();
      event.setType('Type1');
      var event2 = new gd.BaseEvent();
      event2.setType('Type2');

      expect(event.getType()).toBe('Type1');
      expect(event2.getType()).toBe('Type2');

      event.delete();
      event2.delete();
    });

    it('can be cloned', function () {
      var event = new gd.BaseEvent();
      event.setType('Type1');
      var event2 = event.clone();

      expect(event.getType()).toBe('Type1');
      expect(event2.getType()).toBe('Type1');

      event.delete();
      event2.delete();
    });

    it('can be de/serialized', function () {
      var event = new gd.BaseEvent();
      expect(typeof event.serializeTo).toBe('function');
      expect(typeof event.unserializeFrom).toBe('function');
      event.delete();
    });
  });

  describe('gd.ArbitraryEventsWorker', function () {
    describe('gd.EventsParametersLister', function () {
      it('can list parameters and their types', function () {
        var project = new gd.ProjectHelper.createNewGDJSProject();
        var list = new gd.EventsList();

        var evt = new gd.StandardEvent();
        var actions = evt.getActions();
        var act = new gd.Instruction();
        act.setType('Delete');
        act.setParametersCount(1);
        act.setParameter(0, 'MyObject');
        actions.push_back(act);
        evt = list.insertEvent(evt, 0);

        var subEvt = new gd.StandardEvent();
        var conditions = subEvt.getConditions();
        var cnd = new gd.Instruction();
        cnd.setType('PosX');
        cnd.setParametersCount(3);
        cnd.setParameter(0, 'MyObject');
        cnd.setParameter(1, '<');
        cnd.setParameter(2, '300');
        conditions.push_back(cnd);
        evt.getSubEvents().insertEvent(subEvt, 0);

        var parametersLister = new gd.EventsParametersLister(project);
        parametersLister.launch(list);

        expect(parametersLister.getParametersAndTypes().keys().size()).toBe(3);
        expect(parametersLister.getParametersAndTypes().get('MyObject')).toBe(
          'object'
        );
        expect(parametersLister.getParametersAndTypes().get('300')).toBe(
          'expression'
        );

        project.delete();
        list.delete();
      });
    });
  });

  describe('gd.GroupEvent', function () {
    it('handle basic properties', function () {
      const evt = new gd.GroupEvent();
      evt.setName('MyName');
      evt.setSource('http://source.url');
      evt.setCreationTimestamp(150);
      expect(evt.getName()).toBe('MyName');
      expect(evt.getSource()).toBe('http://source.url');
      expect(evt.getCreationTimestamp()).toBe(150);
    });
    it('can be folded', function () {
      const evt = new gd.GroupEvent();
      expect(evt.isFolded()).toBe(false);
      evt.setFolded(true);
      expect(evt.isFolded()).toBe(true);
    });
    it('can remember parameters used to create the group from a template event', function () {
      const evt = new gd.GroupEvent();
      var parameters = evt.getCreationParameters();
      parameters.push_back('Param1');
      parameters.push_back('Param2');

      expect(evt.getCreationParameters().size()).toBe(2);
      expect(evt.getCreationParameters().get(0)).toBe('Param1');
      expect(evt.getCreationParameters().get(1)).toBe('Param2');

      parameters.clear();
      expect(evt.getCreationParameters().size()).toBe(0);
      parameters.push_back('Param1');
      expect(evt.getCreationParameters().get(0)).toBe('Param1');
    });
  });

  describe('gd.StandardEvent', function () {
    it('initial values', function () {
      const evt = new gd.StandardEvent();
      expect(evt.canHaveSubEvents()).toBe(true);
      expect(evt.isExecutable()).toBe(true);
    });
    it('conditions and actions', function () {
      const evt = new gd.StandardEvent();
      var conditions = evt.getConditions();
      expect(evt.getConditions().size()).toBe(0);
      var cnd = new gd.Instruction();
      conditions.push_back(cnd);
      expect(evt.getConditions().size()).toBe(1);

      var actions = evt.getActions();
      expect(evt.getActions().size()).toBe(0);
      var act = new gd.Instruction();
      actions.push_back(act);
      expect(evt.getActions().size()).toBe(1);
    });

    afterAll(function () {
      evt.delete();
    });
  });
  describe('gd.CommentEvent', function () {
    it('initial values', function () {
      const evt = new gd.CommentEvent();
      expect(evt.canHaveSubEvents()).toBe(false);
      expect(evt.isExecutable()).toBe(false);
    });
    it('can have a comment', function () {
      const evt = new gd.CommentEvent();
      evt.setComment('My nice comment about my events!');
      expect(evt.getComment()).toBe('My nice comment about my events!');
    });
    it('can have a comment with UTF8 characters', function () {
      const evt = new gd.CommentEvent();
      evt.setComment('Hello 官话 world!');
      expect(evt.getComment()).toBe('Hello 官话 world!');
    });
    it('can have a background color', function () {
      const evt = new gd.CommentEvent();
      evt.setBackgroundColor(100, 200, 255);
      expect(evt.getBackgroundColorRed()).toBe(100);
      expect(evt.getBackgroundColorGreen()).toBe(200);
      expect(evt.getBackgroundColorBlue()).toBe(255);
    });
    it('can have a text color', function () {
      const evt = new gd.CommentEvent();
      evt.setTextColor(101, 201, 254);
      expect(evt.getTextColorRed()).toBe(101);
      expect(evt.getTextColorGreen()).toBe(201);
      expect(evt.getTextColorBlue()).toBe(254);
    });
  });

  describe('gd.SpriteObject', function () {
    it('is a gd.Object and can have tags', function () {
      var object = new gd.SpriteObject('MySpriteObject');

      expect(object instanceof gd.Object).toBe(true);
      object.setTags('tag1, tag2, tag3');
      expect(object.getTags()).toBe('tag1, tag2, tag3');
      expect(object.getVariables()).toBeTruthy();
      object.delete();
    });

    it('can have animations', function () {
      var obj = new gd.SpriteObject('MySpriteObject');
      obj.addAnimation(new gd.Animation());
      obj.addAnimation(new gd.Animation());
      expect(obj.getAnimationsCount()).toBe(2);
      obj.removeAnimation(1);
      expect(obj.getAnimationsCount()).toBe(1);
    });

    it('can swap animations', function () {
      var obj = new gd.SpriteObject('MySpriteObject');
      obj.removeAllAnimations();
      var anim1 = new gd.Animation();
      var anim2 = new gd.Animation();
      var sprite1 = new gd.Sprite();
      var sprite2 = new gd.Sprite();

      sprite1.setImageName('image1');
      sprite2.setImageName('image2');

      anim1.setDirectionsCount(1);
      anim2.setDirectionsCount(1);
      anim1.getDirection(0).addSprite(sprite1);
      anim2.getDirection(0).addSprite(sprite2);

      obj.addAnimation(anim1);
      obj.addAnimation(anim2);
      expect(
        obj.getAnimation(0).getDirection(0).getSprite(0).getImageName()
      ).toBe('image1');
      obj.swapAnimations(0, 1);
      expect(
        obj.getAnimation(0).getDirection(0).getSprite(0).getImageName()
      ).toBe('image2');
    });

    describe('gd.Direction', function () {
      it('can swap sprites', function () {
        const direction = new gd.Direction();
        const sprite1 = new gd.Sprite();
        const sprite2 = new gd.Sprite();
        sprite1.setImageName('image1');
        sprite2.setImageName('image2');
        direction.addSprite(sprite1);
        direction.addSprite(sprite2);

        expect(direction.getSprite(0).getImageName()).toBe('image1');
        direction.swapSprites(0, 1);
        expect(direction.getSprite(0).getImageName()).toBe('image2');
        direction.swapSprites(1, 0);
        expect(direction.getSprite(0).getImageName()).toBe('image1');
        direction.delete();
      });

      it('can move sprites', function () {
        const direction = new gd.Direction();
        const sprite1 = new gd.Sprite();
        const sprite2 = new gd.Sprite();
        const sprite3 = new gd.Sprite();
        sprite1.setImageName('image1');
        sprite2.setImageName('image2');
        sprite3.setImageName('image3');
        direction.addSprite(sprite1);
        direction.addSprite(sprite2);
        direction.addSprite(sprite3);

        expect(direction.getSprite(0).getImageName()).toBe('image1');
        direction.moveSprite(0, 2);
        expect(direction.getSprite(0).getImageName()).toBe('image2');
        expect(direction.getSprite(1).getImageName()).toBe('image3');
        expect(direction.getSprite(2).getImageName()).toBe('image1');
        direction.swapSprites(1, 1);
        expect(direction.getSprite(0).getImageName()).toBe('image2');
        expect(direction.getSprite(1).getImageName()).toBe('image3');
        expect(direction.getSprite(2).getImageName()).toBe('image1');
        direction.swapSprites(1, 0);
        expect(direction.getSprite(0).getImageName()).toBe('image3');
        expect(direction.getSprite(1).getImageName()).toBe('image2');
        expect(direction.getSprite(2).getImageName()).toBe('image1');
        direction.swapSprites(999, 998);
        expect(direction.getSprite(0).getImageName()).toBe('image3');
        expect(direction.getSprite(1).getImageName()).toBe('image2');
        expect(direction.getSprite(2).getImageName()).toBe('image1');
        direction.delete();
      });

      it('can have metadata', function () {
        const direction = new gd.Direction();
        expect(direction.getMetadata()).toBe('');
        direction.setMetadata('{test: 1}');
        expect(direction.getMetadata()).toBe('{test: 1}');
        direction.delete();
      });
    });

    describe('gd.Sprite', function () {
      it('can have default points', function () {
        var sprite1 = new gd.Sprite();
        sprite1.getCenter().setX(2);
        sprite1.getCenter().setY(3);
        sprite1.getOrigin().setX(4);
        sprite1.getOrigin().setY(5);
        expect(sprite1.getCenter().getX()).toBe(2);
        expect(sprite1.getCenter().getY()).toBe(3);
        expect(sprite1.getOrigin().getX()).toBe(4);
        expect(sprite1.getOrigin().getY()).toBe(5);
      });

      it('can have custom points', function () {
        var sprite1 = new gd.Sprite();
        var point = new gd.Point('test');
        sprite1.addPoint(point);
        point.delete();

        expect(sprite1.hasPoint('test')).toBe(true);
        sprite1.getPoint('test').setX(1);
        sprite1.getPoint('test').setY(2);
        expect(sprite1.getPoint('test').getX()).toBe(1);
        expect(sprite1.getPoint('test').getY()).toBe(2);
      });
    });
  });

  describe('gd.MetadataProvider', function () {
    it('can return metadata about expressions (even if they do not exist)', function () {
      expect(
        gd.MetadataProvider.getExpressionMetadata(
          gd.JsPlatform.get(),
          'NotExistingExpression'
        ).getFullName()
      ).toBe('');
    });

    describe('gd.ObjectMetadata', function () {
      it('can return standard information about Sprite object', function () {
        var objMetadata = gd.MetadataProvider.getObjectMetadata(
          gd.JsPlatform.get(),
          'Sprite'
        );

        expect(objMetadata.getName()).toBe('Sprite');
        expect(objMetadata.getFullName()).toBe('Sprite');
        expect(objMetadata.getDescription().length).not.toBe(0);
        expect(objMetadata.getIconFilename().length).not.toBe(0);
      });
    });
    describe('gd.BehaviorMetadata', function () {
      it('have standard methods to get information', function () {
        var autoMetadata = gd.MetadataProvider.getBehaviorMetadata(
          gd.JsPlatform.get(),
          'NotExistingBehavior'
        );

        expect(autoMetadata.getFullName).not.toBe(undefined);
        expect(autoMetadata.getDefaultName).not.toBe(undefined);
        expect(autoMetadata.getDescription).not.toBe(undefined);
        expect(autoMetadata.getGroup).not.toBe(undefined);
        expect(autoMetadata.getIconFilename).not.toBe(undefined);
        expect(autoMetadata.getObjectType).not.toBe(undefined);
      });
    });
    describe('gd.EffectMetadata', function () {
      it('have standard methods to get information', function () {
        var autoMetadata = gd.MetadataProvider.getEffectMetadata(
          gd.JsPlatform.get(),
          'NotExistingEffect'
        );

        expect(autoMetadata.getType).not.toBe(undefined);
        expect(autoMetadata.getFullName).not.toBe(undefined);
        expect(autoMetadata.getDescription).not.toBe(undefined);
        expect(autoMetadata.getProperties).not.toBe(undefined);
      });
    });
  });

  describe('gd.ResourcesMergingHelper (and gd.AbstractFileSystemJS)', function () {
    it('should export files of the project', function () {
      // Create a project with a mix of resources
      const project = new gd.ProjectHelper.createNewGDJSProject();
      const layout = project.insertNewLayout('Scene', 0);
      const resource = new gd.ImageResource();
      const resource2 = new gd.ImageResource();
      const resource3 = new gd.ImageResource();
      const resource4 = new gd.ImageResource();
      const resource5 = new gd.ImageResource();
      resource.setName('MyResource');
      resource.setFile('MyResource.png');
      resource2.setName('MyAudioResource');
      resource2.setFile('MyResource.wav');
      resource3.setName('test/MyResource.png');
      resource3.setFile('test/MyResource.png');
      resource4.setName('test/MyResourceWithExtension');
      resource4.setFile('test/MyResourceWithExtension');
      resource5.setName('test/sub/folder/MyResourceWithExtension');
      resource5.setFile('test/sub/folder/MyResourceWithExtension');
      project.getResourcesManager().addResource(resource);
      project.getResourcesManager().addResource(resource2);
      project.getResourcesManager().addResource(resource3);
      project.getResourcesManager().addResource(resource4);
      project.getResourcesManager().addResource(resource5);

      // Create a fake file system
      const fs = new gd.AbstractFileSystemJS();
      fs.mkDir = fs.clearDir = function () {};
      fs.getTempDir = function (path) {
        return '/tmp/';
      };
      fs.fileNameFrom = function (fullpath) {
        return path.posix.basename(fullpath);
      };
      fs.dirNameFrom = function (fullpath) {
        return path.posix.dirname(fullpath);
      };
      fs.makeAbsolute = function (relativePath, baseDirectory) {
        return path.posix.resolve(baseDirectory, relativePath);
      };
      fs.makeRelative = function (absolutePath, baseDirectory) {
        return path.posix.relative(baseDirectory, absolutePath);
      };

      // Check that ResourcesMergingHelper can update the filenames
      const resourcesMergingHelper = new gd.ResourcesMergingHelper(fs);
      resourcesMergingHelper.setBaseDirectory('/my/project/');
      project.exposeResources(resourcesMergingHelper);

      const oldAndNewFilenames = resourcesMergingHelper.getAllResourcesOldAndNewFilename();
      expect(oldAndNewFilenames.get('/my/project/MyResource.png')).toBe(
        'MyResource.png'
      );
      expect(oldAndNewFilenames.get('/my/project/MyResource.wav')).toBe(
        'MyResource.wav'
      );
      expect(oldAndNewFilenames.get('/my/project/test/MyResource.png')).toBe(
        'MyResource2.png'
      );
      expect(
        oldAndNewFilenames.get('/my/project/test/MyResourceWithExtension')
      ).toBe('MyResourceWithExtension');
      expect(
        oldAndNewFilenames.get(
          '/my/project/test/sub/folder/MyResourceWithExtension'
        )
      ).toBe('MyResourceWithExtension2');

      resourcesMergingHelper.delete();
      project.delete();
    });
  });

  describe('gd.ProjectResourcesCopier (and gd.AbstractFileSystemJS)', function () {
    it('should export files of the project', function () {
      // Create a project with a mix of resources, stored in /my/project folder.
      const project = new gd.ProjectHelper.createNewGDJSProject();
      project.setProjectFile('/my/project/project.json');
      const layout = project.insertNewLayout('Scene', 0);
      const resource = new gd.ImageResource();
      const resource2 = new gd.ImageResource();
      const resource3 = new gd.ImageResource();
      const resource4 = new gd.ImageResource();
      const resource5 = new gd.ImageResource();
      resource.setName('MyResource');
      resource.setFile('MyResource.png');
      resource2.setName('MyAudioResource');
      resource2.setFile('MyResource.wav');
      resource3.setName('MyAbsoluteResource');
      resource3.setFile('/my/absolute/path/MyResource2.png');
      resource4.setName('test/MyResourceWithoutExtension');
      resource4.setFile('test/MyResourceWithoutExtension');
      resource5.setName('test/sub/folder/MyResourceWithoutExtension');
      resource5.setFile('test/sub/folder/MyResourceWithoutExtension'); // Same filename as resource4
      project.getResourcesManager().addResource(resource);
      project.getResourcesManager().addResource(resource2);
      project.getResourcesManager().addResource(resource3);
      project.getResourcesManager().addResource(resource4);
      project.getResourcesManager().addResource(resource5);

      // Create a fake file system
      const fs = new gd.AbstractFileSystemJS();
      fs.mkDir = fs.clearDir = function () {};
      fs.getTempDir = function (path) {
        return '/tmp/';
      };
      fs.fileNameFrom = function (fullPath) {
        return path.posix.basename(fullPath);
      };
      fs.dirNameFrom = function (fullPath) {
        return path.posix.dirname(fullPath);
      };
      fs.makeAbsolute = function (relativePath, baseDirectory) {
        return path.posix.resolve(baseDirectory, relativePath);
      };
      fs.makeRelative = function (absolutePath, baseDirectory) {
        return path.posix.relative(baseDirectory, absolutePath);
      };
      fs.isAbsolute = function (fullPath) {
        return path.posix.isAbsolute(fullPath);
      };
      fs.dirExists = function (directoryPath) {
        return true; // Fake that all directory required exist.
      };

      // In particular, create a mock copyFile, that we can track to verify
      // files are properly copied.
      fs.copyFile = jest.fn();
      fs.copyFile.mockImplementation(function (srcPath, destPath) {
        return true;
      });

      // Check that resources can be copied to another folder:
      // * including absolute files.
      // * preserving relative file structures
      fs.copyFile.mockClear();
      gd.ProjectResourcesCopier.copyAllResourcesTo(
        project,
        fs,
        '/my/new/folder',
        false,
        false,
        true
      );
      expect(fs.copyFile).toHaveBeenCalledTimes(5); // All 5 resources are copied
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/MyResource.png',
        '/my/new/folder/MyResource.png'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/MyResource.wav',
        '/my/new/folder/MyResource.wav'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/absolute/path/MyResource2.png',
        '/my/new/folder/MyResource2.png'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/test/MyResourceWithoutExtension',
        '/my/new/folder/test/MyResourceWithoutExtension'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/test/sub/folder/MyResourceWithoutExtension',
        '/my/new/folder/test/sub/folder/MyResourceWithoutExtension'
      );

      // Check that resources can be copied to another folder:
      // * including absolute files.
      // * NOT preserving relative file structures
      // Check that filename collisions are avoided.
      fs.copyFile.mockClear();
      gd.ProjectResourcesCopier.copyAllResourcesTo(
        project,
        fs,
        '/my/new/folder',
        false,
        false,
        false
      );
      expect(fs.copyFile).toHaveBeenCalledTimes(5); // All 5 resources are copied
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/MyResource.png',
        '/my/new/folder/MyResource.png'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/MyResource.wav',
        '/my/new/folder/MyResource.wav'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/absolute/path/MyResource2.png',
        '/my/new/folder/MyResource2.png'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/test/MyResourceWithoutExtension',
        '/my/new/folder/MyResourceWithoutExtension'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/test/sub/folder/MyResourceWithoutExtension',
        '/my/new/folder/MyResourceWithoutExtension2'
      );

      // Check that resources can be copied to another folder:
      // * without touching absolute files.
      // * preserving relative file structures
      fs.copyFile.mockClear();
      gd.ProjectResourcesCopier.copyAllResourcesTo(
        project,
        fs,
        '/my/new/folder',
        false,
        true,
        true
      );
      expect(fs.copyFile).toHaveBeenCalledTimes(4); // Only the 4 relative resources are copied
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/MyResource.png',
        '/my/new/folder/MyResource.png'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/MyResource.wav',
        '/my/new/folder/MyResource.wav'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/test/MyResourceWithoutExtension',
        '/my/new/folder/test/MyResourceWithoutExtension'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/test/sub/folder/MyResourceWithoutExtension',
        '/my/new/folder/test/sub/folder/MyResourceWithoutExtension'
      );

      // Check that resources can be copied to another folder:
      // * without touching absolute files.
      // * NOT preserving relative file structures
      // Check that filename collisions are avoided.
      fs.copyFile.mockClear();
      gd.ProjectResourcesCopier.copyAllResourcesTo(
        project,
        fs,
        '/my/new/folder',
        false,
        true,
        false
      );
      expect(fs.copyFile).toHaveBeenCalledTimes(5); // All 5 resources are copied
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/MyResource.png',
        '/my/new/folder/MyResource.png'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/MyResource.wav',
        '/my/new/folder/MyResource.wav'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/absolute/path/MyResource2.png',
        '/my/new/folder/MyResource2.png'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/test/MyResourceWithoutExtension',
        '/my/new/folder/MyResourceWithoutExtension'
      );
      expect(fs.copyFile).toHaveBeenCalledWith(
        '/my/project/test/sub/folder/MyResourceWithoutExtension',
        '/my/new/folder/MyResourceWithoutExtension2'
      );

      project.delete();
    });
  });

  describe('gd.Exporter (and gd.AbstractFileSystemJS)', function () {
    it('should export a layout for preview', function (done) {
      var fs = new gd.AbstractFileSystemJS();
      var project = new gd.ProjectHelper.createNewGDJSProject();
      var layout = project.insertNewLayout('Scene', 0);

      fs.mkDir = fs.clearDir = function () {};
      fs.getTempDir = function (path) {
        return '/tmp/';
      };
      fs.fileNameFrom = function (fullpath) {
        return path.basename(fullpath);
      };
      fs.dirNameFrom = function (fullpath) {
        return path.dirname(fullpath);
      };
      fs.readDir = function () {
        return new gd.VectorString();
      };
      fs.writeToFile = function (path, content) {
        //Validate that some code have been generated:
        expect(content).toMatch('runtimeScene.getOnceTriggers().startNewFrame');
        done();
      };

      const exporter = new gd.Exporter(fs);
      const previewExportOptions = new gd.PreviewExportOptions(
        project,
        '/path/for/export/'
      );
      previewExportOptions.setLayoutName('Scene');
      exporter.exportProjectForPixiPreview(previewExportOptions);
      previewExportOptions.delete();
      exporter.delete();
    });
  });

  describe('gd.EventsRemover', function () {
    it('should remove events', function () {
      var list = new gd.EventsList();
      var event1 = list.insertEvent(new gd.StandardEvent(), 0);
      var event2 = list.insertEvent(new gd.StandardEvent(), 1);
      var event3 = list.insertEvent(new gd.StandardEvent(), 2);

      var remover = new gd.EventsRemover();
      remover.addEventToRemove(event1);
      remover.addEventToRemove(event3);
      remover.launch(list);

      expect(list.getEventsCount()).toBe(1);
      expect(list.getEventAt(0)).toBe(event2);
    });
  });

  describe('gd.WholeProjectRefactorer', function () {
    it('should rename and delete an object', function () {
      var project = new gd.ProjectHelper.createNewGDJSProject();
      var layout = project.insertNewLayout('Scene', 0);
      var instance1 = layout.getInitialInstances().insertNewInitialInstance();
      var instance2 = layout.getInitialInstances().insertNewInitialInstance();
      instance1.setObjectName('Object1');
      instance2.setObjectName('Object2');

      gd.WholeProjectRefactorer.objectOrGroupRenamedInLayout(
        project,
        layout,
        'Object1',
        'Object3',
        /* isObjectGroup=*/ false
      );
      expect(layout.getInitialInstances().hasInstancesOfObject('Object1')).toBe(
        false
      );
      expect(layout.getInitialInstances().hasInstancesOfObject('Object2')).toBe(
        true
      );
      expect(layout.getInitialInstances().hasInstancesOfObject('Object3')).toBe(
        true
      );

      gd.WholeProjectRefactorer.objectOrGroupRemovedInLayout(
        project,
        layout,
        'Object3',
        /* isObjectGroup=*/ false,
        true
      );
      expect(layout.getInitialInstances().hasInstancesOfObject('Object1')).toBe(
        false
      );
      expect(layout.getInitialInstances().hasInstancesOfObject('Object2')).toBe(
        true
      );
      expect(layout.getInitialInstances().hasInstancesOfObject('Object3')).toBe(
        false
      );
    });
    // See other tests in WholeProjectRefactorer.cpp
  });

  describe('gd.ExpressionParser2 and gd.ExpressionValidator', function () {
    let project = null;
    let layout = null;
    beforeAll(() => {
      project = new gd.ProjectHelper.createNewGDJSProject();
      layout = project.insertNewLayout('Scene', 0);
      layout.insertNewObject(project, 'Sprite', 'MySpriteObject', 0);
    });

    function testExpression(
      type,
      expression,
      expectedError,
      expectedErrorPosition
    ) {
      const parser = new gd.ExpressionParser2(
        gd.JsPlatform.get(),
        project,
        layout
      );
      const expressionNode = parser.parseExpression(type, expression).get();

      const expressionValidator = new gd.ExpressionValidator();
      expressionNode.visit(expressionValidator);
      if (expectedError) {
        expect(expressionValidator.getErrors().size()).toBe(1);
        expect(expressionValidator.getErrors().at(0).getMessage()).toBe(
          expectedError
        );
        if (expectedErrorPosition)
          expect(expressionValidator.getErrors().at(0).getStartPosition()).toBe(
            expectedErrorPosition
          );
      } else {
        expect(expressionValidator.getErrors().size()).toBe(0);
      }

      expressionValidator.delete();
      parser.delete();
    }

    it('can parse valid expressions (number)', function () {
      testExpression('number', '1+1');
      testExpression('number', '2-3');
      testExpression('number', '4/5');
      testExpression('number', '6*7');
      testExpression('number', '8 + 9');
      testExpression('number', '10 +    11');
      testExpression('number', '12 +    13 - 14');
      testExpression('number', '  15 +    16 - 17   ');
      testExpression('number', '.14');
      testExpression('number', '3.');
    });

    it('can parse valid expressions (string)', function () {
      testExpression('string', '"Hello"');
      testExpression('string', '"Hello" + " " + "World"');
    });

    it('can parse valid expressions ("number|string" type)', function () {
      testExpression('number|string', '1+1');
      testExpression('number|string', '2-3');
      testExpression('number|string', '4/5');
      testExpression('number|string', '6*7');
      testExpression('number|string', '"Hello"');
      testExpression('number|string', '"Hello" + " " + "World"');
    });

    it('report errors in invalid expressions', function () {
      testExpression(
        'number',
        '1//2',
        'You must enter a number or a valid expression call.',
        2
      );
      testExpression('number', 'bad expression', 'You must enter a number.', 0);
      testExpression(
        'number',
        '1 + test()',
        "Cannot find an expression with this name: test\nDouble check that you've not made any typo in the name.",
        4
      );
      testExpression(
        'number',
        '3..14',
        'No operator found. Did you forget to enter an operator (like +, -, * or /) between numbers or expressions?',
        2
      );
      testExpression(
        'string',
        '="Mynewscene"',
        'You must enter a text (between quotes) or a valid expression call.',
        0
      );
    });
    it('report errors in invalid expressions ("number|string" type)', function () {
      testExpression(
        'number|string',
        '123 + "World"',
        'You entered a text, but a number was expected.',
        6
      );
      testExpression(
        'number|string',
        '"World" + 123',
        'You entered a number, but a text was expected (in quotes).',
        10
      );
      testExpression(
        'number|string',
        '"World" + ToNumber("123")',
        'You tried to use an expression that returns a number, but a string is expected. Use `ToString` if you need to convert a number to a string.',
        10
      );
      testExpression(
        'number|string',
        '123 + ToString(456)',
        'You tried to use an expression that returns a string, but a number is expected. Use `ToNumber` if you need to convert a string to a number.',
        6
      );
    });

    it('can parse valid expressions with free functions', function () {
      testExpression('number', '1+sin(3.14)');
      testExpression('number', 'abs(-5)');
      testExpression('number', 'abs(-5) + cos(sin(3))');
      testExpression('number', 'atan2(-5, 3)');
      testExpression('number', 'MouseX("", 0) + 1');
    });

    it('can report errors when using too much arguments', function () {
      testExpression(
        'number',
        'abs(-5, 3)',
        "This parameter was not expected by this expression. Remove it or verify that you've entered the proper expression name."
      );
      testExpression(
        'number',
        'MouseX("", 0, 0) + 1',
        "This parameter was not expected by this expression. Remove it or verify that you've entered the proper expression name."
      );
    });

    it('can parse valid expressions with free functions having optional parameters', function () {
      testExpression('number', 'MouseX() + 1');
      testExpression('number', 'MouseX("") + 1');
    });

    it('can parse expressions with objects functions', function () {
      testExpression('number', 'MySpriteObject.X()');
      testExpression('number', 'MySpriteObject.X() + 1');
      testExpression('number', 'MySpriteObject.PointX("Point")');
    });

    it('can report errors when using too much arguments in object functions', function () {
      testExpression(
        'number',
        'MySpriteObject.PointX("Point", 2)',
        "This parameter was not expected by this expression. Remove it or verify that you've entered the proper expression name."
      );
    });

    it('can parse arguments being expressions', function () {
      testExpression('number', 'MouseX(VariableString(myVariable), 0) + 1');
    });
  });

  describe('gd.ExpressionCompletionFinder', function () {
    let project = null;
    let layout = null;
    beforeAll(() => {
      project = new gd.ProjectHelper.createNewGDJSProject();
      layout = project.insertNewLayout('Scene', 0);
      layout.insertNewObject(project, 'Sprite', 'MySpriteObject', 0);
    });

    function testCompletions(
      type,
      expressionWithCaret,
      onCompletionDescription
    ) {
      const caretPosition = expressionWithCaret.indexOf('|');
      if (caretPosition === -1) {
        throw new Error(
          'Caret location not found in expression: ' + expressionWithCaret
        );
      }
      const expression = expressionWithCaret.replace('|', '');

      const parser = new gd.ExpressionParser2(
        gd.JsPlatform.get(),
        project,
        layout
      );
      const expressionNode = parser.parseExpression(type, expression).get();
      const completionDescriptions = gd.ExpressionCompletionFinder.getCompletionDescriptionsFor(
        expressionNode,
        // We're looking for completion for the character just before the caret.
        Math.max(0, caretPosition - 1)
      );

      for (let i = 0; i < completionDescriptions.size(); i++) {
        const completionDescription = completionDescriptions.at(i);

        onCompletionDescription(completionDescription, i);
      }

      parser.delete();
    }

    it('completes an empty expression', function () {
      expect.assertions(6);
      testCompletions('number', '|', (completionDescription, index) => {
        if (index === 0) {
          expect(completionDescription.getCompletionKind()).toBe(
            gd.ExpressionCompletionDescription.Object
          );
          expect(completionDescription.getType()).toBe('number');
          expect(completionDescription.getPrefix()).toBe('');
        } else {
          expect(completionDescription.getCompletionKind()).toBe(
            gd.ExpressionCompletionDescription.Expression
          );
          expect(completionDescription.getType()).toBe('number');
          expect(completionDescription.getPrefix()).toBe('');
        }
      });
    });

    it('completes an expression with an operator', function () {
      expect.assertions(6);
      testCompletions('number', '1 +| ', (completionDescription, index) => {
        if (index === 0) {
          expect(completionDescription.getCompletionKind()).toBe(
            gd.ExpressionCompletionDescription.Object
          );
          expect(completionDescription.getType()).toBe('number');
          expect(completionDescription.getPrefix()).toBe('');
        } else {
          expect(completionDescription.getCompletionKind()).toBe(
            gd.ExpressionCompletionDescription.Expression
          );
          expect(completionDescription.getType()).toBe('number');
          expect(completionDescription.getPrefix()).toBe('');
        }
      });
    });

    it('completes an expression with an operator and a prefix', function () {
      expect.assertions(6);
      testCompletions('number', '1 + My| ', (completionDescription, index) => {
        if (index === 0) {
          expect(completionDescription.getCompletionKind()).toBe(
            gd.ExpressionCompletionDescription.Object
          );
          expect(completionDescription.getType()).toBe('number');
          expect(completionDescription.getPrefix()).toBe('My');
        } else {
          expect(completionDescription.getCompletionKind()).toBe(
            gd.ExpressionCompletionDescription.Expression
          );
          expect(completionDescription.getType()).toBe('number');
          expect(completionDescription.getPrefix()).toBe('My');
        }
      });
    });
    it('completes an expression with a partial object function', function () {
      expect.assertions(8);
      testCompletions(
        'number',
        '1 + MyObject.Func| ',
        (completionDescription, index) => {
          if (index == 0) {
            expect(completionDescription.getCompletionKind()).toBe(
              gd.ExpressionCompletionDescription.Behavior
            );
            expect(completionDescription.getType()).toBe('');
            expect(completionDescription.getPrefix()).toBe('Func');
            expect(completionDescription.getObjectName()).toBe('MyObject');
          } else {
            expect(completionDescription.getCompletionKind()).toBe(
              gd.ExpressionCompletionDescription.Expression
            );
            expect(completionDescription.getType()).toBe('number');
            expect(completionDescription.getPrefix()).toBe('Func');
            expect(completionDescription.getObjectName()).toBe('MyObject');
          }
        }
      );
    });
    it('completes an expression with a partial behavior function', function () {
      expect.assertions(5);
      testCompletions(
        'number',
        '1 + MyObject.MyBehavior::Func| ',
        (completionDescription, index) => {
          expect(completionDescription.getCompletionKind()).toBe(
            gd.ExpressionCompletionDescription.Expression
          );
          expect(completionDescription.getType()).toBe('number');
          expect(completionDescription.getPrefix()).toBe('Func');
          expect(completionDescription.getObjectName()).toBe('MyObject');
          expect(completionDescription.getBehaviorName()).toBe('MyBehavior');
        }
      );
    });
    it('completes an expression parameters', function () {
      expect.assertions(6);
      testCompletions(
        'number',
        '1 + MySpriteObject.PointX(a| ',
        (completionDescription, index) => {
          if (index === 0) {
            expect(completionDescription.getCompletionKind()).toBe(
              gd.ExpressionCompletionDescription.Object
            );
            expect(completionDescription.getType()).toBe('string');
            expect(completionDescription.getPrefix()).toBe('a');
          } else {
            expect(completionDescription.getCompletionKind()).toBe(
              gd.ExpressionCompletionDescription.Expression
            );
            expect(completionDescription.getType()).toBe('string');
            expect(completionDescription.getPrefix()).toBe('a');
          }
        }
      );
    });

    // More tests are done in C++ for ExpressionCompletionFinder.
  });

  describe('gd.Vector2f', function () {
    describe('gd.VectorVector2f', function () {
      it('can be used to manipulate a vector of gd.Vector2f', function () {
        const vectorVector2f = new gd.VectorVector2f();
        const vector2f = new gd.Vector2f();

        vectorVector2f.push_back(vector2f);
        vectorVector2f.push_back(vector2f);
        vectorVector2f.push_back(vector2f);

        expect(vectorVector2f.size()).toBe(3);
        vectorVector2f.at(0).set_x(1);
        vectorVector2f.at(1).set_x(2);
        vectorVector2f.at(2).set_x(3);

        expect(vectorVector2f.at(0).get_x()).toBe(1);
        expect(vectorVector2f.at(1).get_x()).toBe(2);
        expect(vectorVector2f.at(2).get_x()).toBe(3);

        gd.removeFromVectorVector2f(vectorVector2f, 1);
        expect(vectorVector2f.size()).toBe(2);
        expect(vectorVector2f.at(0).get_x()).toBe(1);
        expect(vectorVector2f.at(1).get_x()).toBe(3);

        vectorVector2f.clear();
        expect(vectorVector2f.size()).toBe(0);
      });
    });
  });

  describe('gd.PlatformExtension', function () {
    const makeTestExtension = () => {
      const extension = new gd.PlatformExtension();
      extension
        .setExtensionInformation(
          'TestExtensionName',
          'Full name of test extension',
          'Description of test extension',
          'Author of test extension',
          'License of test extension'
        )
        .setExtensionHelpPath('/path/to/extension/help');
      return extension;
    };

    it('can be created and have basic information filled', function () {
      const extension = makeTestExtension();

      expect(extension.getName()).toBe('TestExtensionName');
      expect(extension.getFullName()).toBe('Full name of test extension');
      expect(extension.getDescription()).toBe('Description of test extension');
      expect(extension.getAuthor()).toBe('Author of test extension');
      expect(extension.getLicense()).toBe('License of test extension');
      expect(extension.getHelpPath()).toBe('/path/to/extension/help');
      extension.delete();
    });

    it('can have actions and conditions added', function () {
      const extension = makeTestExtension();
      extension
        .addCondition(
          'BannerShowing',
          'Banner showing',
          'Check if there is a banner being displayed.',
          'Banner is showing',
          'AdMob',
          'JsPlatform/Extensions/admobicon24.png',
          'JsPlatform/Extensions/admobicon16.png'
        )
        .getCodeExtraInformation()
        .setIncludeFile('Extensions/AdMob/admobtools.js')
        .setFunctionName('gdjs.adMob.isBannerShowing');

      expect(
        extension.getAllConditions().has('TestExtensionName::BannerShowing')
      ).toBe(true);
      const condition = extension
        .getAllConditions()
        .get('TestExtensionName::BannerShowing');
      expect(condition.getFullName()).toBe('Banner showing');
      expect(condition.isHidden()).toBe(false);

      // Check also the API to duplicate a condition.
      extension
        .addDuplicatedCondition('AnotherCondition', 'BannerShowing')
        .setHidden();

      expect(
        extension.getAllConditions().has('TestExtensionName::AnotherCondition')
      ).toBe(true);
      const copiedCondition = extension
        .getAllConditions()
        .get('TestExtensionName::AnotherCondition');
      expect(copiedCondition.getFullName()).toBe('Banner showing');
      expect(copiedCondition.isHidden()).toBe(true);
      extension.delete();
    });
    it('can have expressions and conditions added at the same time', function () {
      const extension = makeTestExtension();
      extension
        .addExpressionAndCondition(
          'number',
          'PlayerHealth',
          'Health of the player',
          'The health of the player, from 0 to 100.',
          'the health of the player',
          'Health Management',
          'SomeHealthIcon.png'
        )
        .addCodeOnlyParameter('currentScene', '')
        .addParameter('string', 'Some stuff', '', false)
        .setParameterLongDescription('Blabla')
        .setFunctionName('some.method.to.getPlayerHealth')
        .useStandardParameters('number');

      expect(
        extension.getAllConditions().has('TestExtensionName::PlayerHealth')
      ).toBe(true);
      const declaredCondition = extension
        .getAllConditions()
        .get('TestExtensionName::PlayerHealth');
      expect(declaredCondition.getParametersCount()).toBe(4);

      expect(
        extension.getAllExpressions().has('TestExtensionName::PlayerHealth')
      ).toBe(true);
      const declaredExpression = extension
        .getAllExpressions()
        .get('TestExtensionName::PlayerHealth');
      expect(declaredExpression.getParametersCount()).toBe(2);
    });
    it('can have expressions, conditions and actions added at the same time', function () {
      const extension = makeTestExtension();
      extension
        .addExpressionAndConditionAndAction(
          'number',
          'PlayerHealth',
          'Health of the player',
          'The health of the player, from 0 to 100.',
          'the health of the player',
          'Health Management',
          'SomeHealthIcon.png'
        )
        .addCodeOnlyParameter('currentScene', '')
        .addParameter('string', 'Some stuff', '', false)
        .setParameterLongDescription('Blabla')
        .setFunctionName('some.method.to.getPlayerHealth')
        .useStandardParameters('number');

      expect(
        extension.getAllConditions().has('TestExtensionName::PlayerHealth')
      ).toBe(true);
      const declaredCondition = extension
        .getAllConditions()
        .get('TestExtensionName::PlayerHealth');
      expect(declaredCondition.getParametersCount()).toBe(4);

      expect(
        extension.getAllActions().has('TestExtensionName::SetPlayerHealth')
      ).toBe(true);
      const declaredAction = extension
        .getAllActions()
        .get('TestExtensionName::SetPlayerHealth');
      expect(declaredAction.getParametersCount()).toBe(4);

      expect(
        extension.getAllExpressions().has('TestExtensionName::PlayerHealth')
      ).toBe(true);
      const declaredExpression = extension
        .getAllExpressions()
        .get('TestExtensionName::PlayerHealth');
      expect(declaredExpression.getParametersCount()).toBe(2);
    });
  });

  describe('gd.Platform (using gd.JsPlatform)', function () {
    it('can have extension added and removed', function () {
      const extension = new gd.PlatformExtension();
      extension.setExtensionInformation(
        'MyNewExtension',
        'Full name of test extension',
        'Description of test extension',
        'Author of test extension',
        'License of test extension'
      );

      gd.JsPlatform.get().addNewExtension(extension);
      expect(gd.JsPlatform.get().isExtensionLoaded('MyNewExtension')).toBe(
        true
      );
      gd.JsPlatform.get().removeExtension('MyNewExtension');
      expect(gd.JsPlatform.get().isExtensionLoaded('MyNewExtension')).toBe(
        false
      );
      gd.JsPlatform.get().addNewExtension(extension);
      gd.JsPlatform.get().addNewExtension(extension);
      expect(gd.JsPlatform.get().isExtensionLoaded('MyNewExtension')).toBe(
        true
      );
      gd.JsPlatform.get().removeExtension('MyNewExtension');
      expect(gd.JsPlatform.get().isExtensionLoaded('MyNewExtension')).toBe(
        false
      );
      extension.delete();
    });

    it('has a namespace separator', function () {
      expect(gd.PlatformExtension.getNamespaceSeparator()).toBe('::');
    });
  });

  describe('gd.ParameterMetadata', function () {
    it('can tell the type of a parameter', function () {
      expect(gd.ParameterMetadata.isObject('object')).toBe(true);
      expect(gd.ParameterMetadata.isObject('objectPtr')).toBe(true);
      expect(gd.ParameterMetadata.isObject('123')).toBe(false);
      expect(gd.ParameterMetadata.isBehavior('behavior')).toBe(true);
      expect(gd.ParameterMetadata.isBehavior('behavior34234')).toBe(false);
    });
    it('can have attributes and be serialized', function () {
      const parameter1 = new gd.ParameterMetadata();
      parameter1.setType('objectList');
      parameter1.setName('MyObjectWithoutType');
      parameter1.setDescription('The first object to be used');
      parameter1.setLongDescription('A long description');
      parameter1.setDefaultValue('Default value');
      parameter1.setOptional(true);

      const serializerElement = new gd.SerializerElement();
      parameter1.serializeTo(serializerElement);

      const parameter2 = new gd.ParameterMetadata();
      parameter2.unserializeFrom(serializerElement);

      expect(parameter1.getType()).toBe('objectList');
      expect(parameter1.getName()).toBe('MyObjectWithoutType');
      expect(parameter1.getDescription()).toBe('The first object to be used');
      expect(parameter1.getLongDescription()).toBe('A long description');
      expect(parameter1.getDefaultValue()).toBe('Default value');
      expect(parameter1.isOptional()).toBe(true);
    });
  });

  describe('gd.ParameterMetadataTools', function () {
    it('can create an object container from parameters', function () {
      const project = gd.ProjectHelper.createNewGDJSProject();

      const parameters = new gd.VectorParameterMetadata();
      const parameter1 = new gd.ParameterMetadata();
      parameter1.setType('objectList');
      parameter1.setName('MyObjectWithoutType');
      parameter1.setDescription('The first object to be used');
      const parameter2 = new gd.ParameterMetadata();
      parameter2.setType('expression');
      parameter2.setName('MyNumber');
      parameter2.setDescription('Some number');
      const parameter3 = new gd.ParameterMetadata();
      parameter3.setType('objectList');
      // parameter3.setName(''); No name for this parameter
      parameter3.setDescription(
        'This parameter will be skipped, as it has no name'
      );
      parameter3.setExtraInfo('Sprite');
      const parameter4 = new gd.ParameterMetadata();
      parameter4.setType('string');
      parameter4.setName('MyString');
      parameter4.setDescription('Some string');
      const parameter5 = new gd.ParameterMetadata();
      parameter5.setType('objectList');
      parameter5.setName('MySpriteObject');
      parameter5.setDescription('The second object to be used, a sprite');
      parameter5.setExtraInfo('Sprite');

      parameters.push_back(parameter1);
      parameters.push_back(parameter2);
      parameters.push_back(parameter3);
      parameters.push_back(parameter4);
      parameters.push_back(parameter5);

      parameters.push_back(parameter5);
      expect(parameters.size()).toBe(6);
      gd.removeFromVectorParameterMetadata(parameters, 5);
      expect(parameters.size()).toBe(5);

      objectsContainer = new gd.ObjectsContainer();
      gd.ParameterMetadataTools.parametersToObjectsContainer(
        project,
        parameters,
        objectsContainer
      );

      expect(objectsContainer.getObjectsCount()).toBe(2);
      expect(objectsContainer.hasObjectNamed('MyObjectWithoutType')).toBe(true);
      expect(objectsContainer.getObject('MyObjectWithoutType').getType()).toBe(
        ''
      );
      expect(objectsContainer.hasObjectNamed('MySpriteObject')).toBe(true);
      expect(objectsContainer.getObject('MySpriteObject').getType()).toBe(
        'Sprite'
      );

      project.delete();
    });

    it('can give the previous object parameter', function () {
      const parameters = new gd.VectorParameterMetadata();
      const parameter1 = new gd.ParameterMetadata();
      parameter1.setType('objectList');
      const parameter2 = new gd.ParameterMetadata();
      parameter2.setType('behavior');
      const parameter3 = new gd.ParameterMetadata();
      parameter3.setType('objectList');
      const parameter4 = new gd.ParameterMetadata();
      parameter4.setType('string');
      const parameter5 = new gd.ParameterMetadata();
      parameter5.setType('objectvar');
      const parameter6 = new gd.ParameterMetadata();
      parameter6.setType('objectvar');

      parameters.push_back(parameter1);
      parameters.push_back(parameter2);
      parameters.push_back(parameter3);
      parameters.push_back(parameter4);
      parameters.push_back(parameter5);
      parameters.push_back(parameter6);

      objectsContainer = new gd.ObjectsContainer();
      expect(
        gd.ParameterMetadataTools.getObjectParameterIndexFor(parameters, 0)
      ).toBe(0);
      expect(
        gd.ParameterMetadataTools.getObjectParameterIndexFor(parameters, 1)
      ).toBe(0);
      expect(
        gd.ParameterMetadataTools.getObjectParameterIndexFor(parameters, 2)
      ).toBe(2);
      expect(
        gd.ParameterMetadataTools.getObjectParameterIndexFor(parameters, 3)
      ).toBe(2);
      expect(
        gd.ParameterMetadataTools.getObjectParameterIndexFor(parameters, 4)
      ).toBe(2);
      expect(
        gd.ParameterMetadataTools.getObjectParameterIndexFor(parameters, 5)
      ).toBe(2);
      expect(
        gd.ParameterMetadataTools.getObjectParameterIndexFor(parameters, 999)
      ).toBe(-1);
    });
  });

  describe('gd.EventsFunction', () => {
    it('can store events', function () {
      const project = gd.ProjectHelper.createNewGDJSProject();
      const eventsFunction = new gd.EventsFunction();
      const events = eventsFunction.getEvents();
      expect(events.getEventsCount()).toBe(0);
      var evt = events.insertNewEvent(
        project,
        'BuiltinCommonInstructions::Standard',
        0
      );
      expect(events.getEventsCount()).toBe(1);
      eventsFunction.delete();
      project.delete();
    });
    it('can have a name, fullname and description', function () {
      const eventsFunction = new gd.EventsFunction();
      eventsFunction.setName('My name');
      eventsFunction.setFullName('My descriptive name');
      eventsFunction.setDescription('My description');
      expect(eventsFunction.getName()).toBe('My name');
      expect(eventsFunction.getFullName()).toBe('My descriptive name');
      expect(eventsFunction.getDescription()).toBe('My description');
      eventsFunction.delete();
    });
  });

  describe('gd.EventsFunctionsExtension', () => {
    it('can have a namespace, version, name, fullname, description', function () {
      const eventsFunctionsExtension = new gd.EventsFunctionsExtension();
      eventsFunctionsExtension.setNamespace('MyExt');
      eventsFunctionsExtension.setVersion('1.1');
      eventsFunctionsExtension.setName('My name');
      eventsFunctionsExtension.setFullName('My descriptive name');
      eventsFunctionsExtension.setDescription('My description');
      eventsFunctionsExtension.setIconUrl('data:image/png;base64,iVBetcetc');
      eventsFunctionsExtension.setPreviewIconUrl(
        'http://resources.gdevelop-app.com/test'
      );
      expect(eventsFunctionsExtension.getNamespace()).toBe('MyExt');
      expect(eventsFunctionsExtension.getVersion()).toBe('1.1');
      expect(eventsFunctionsExtension.getName()).toBe('My name');
      expect(eventsFunctionsExtension.getFullName()).toBe(
        'My descriptive name'
      );
      expect(eventsFunctionsExtension.getDescription()).toBe('My description');
      expect(eventsFunctionsExtension.getPreviewIconUrl()).toBe(
        'http://resources.gdevelop-app.com/test'
      );

      const eventsFunction = eventsFunctionsExtension.insertNewEventsFunction(
        'MyFunction',
        0
      );
      expect(
        eventsFunctionsExtension.hasEventsFunctionNamed('MyFunction')
      ).toBe(true);
      expect(
        eventsFunctionsExtension.hasEventsFunctionNamed('MyNotExistingFunction')
      ).toBe(false);
      expect(eventsFunctionsExtension.getEventsFunctionsCount()).toBe(1);
      expect(eventsFunctionsExtension.getEventsFunctionAt(0).getName()).toBe(
        'MyFunction'
      );
      expect(
        eventsFunctionsExtension.getEventsFunction('MyFunction').getName()
      ).toBe('MyFunction');

      eventsFunctionsExtension.delete();
    });
    it('can have events based behaviors', function () {
      const eventsFunctionsExtension = new gd.EventsFunctionsExtension();
      expect(
        eventsFunctionsExtension.getEventsBasedBehaviors().getCount()
      ).toBe(0);

      eventsFunctionsExtension
        .getEventsBasedBehaviors()
        .insertNew('MyBehavior1', 0);
      eventsFunctionsExtension
        .getEventsBasedBehaviors()
        .insertNew('MyBehavior2', 1);
      expect(
        eventsFunctionsExtension.getEventsBasedBehaviors().getCount()
      ).toBe(2);

      expect(
        eventsFunctionsExtension.getEventsBasedBehaviors().has('MyBehavior1')
      ).toBe(true);
      expect(
        eventsFunctionsExtension.getEventsBasedBehaviors().has('MyBehavior2')
      ).toBe(true);
      expect(
        eventsFunctionsExtension.getEventsBasedBehaviors().has('MyBehavior3')
      ).toBe(false);
      expect(
        eventsFunctionsExtension.getEventsBasedBehaviors().getAt(1).getName()
      ).toBe('MyBehavior2');
      expect(
        eventsFunctionsExtension
          .getEventsBasedBehaviors()
          .get('MyBehavior1')
          .getName()
      ).toBe('MyBehavior1');
    });
    it('can be unserialized, with tags as a (deprecated) string', function () {
      const project = gd.ProjectHelper.createNewGDJSProject();
      const eventsFunctionsExtension = new gd.EventsFunctionsExtension();

      // Check that we can unserialize tags from the old format, where they were
      // written as a string.
      const element = gd.Serializer.fromJSObject({
        version: '1.0.0',
        extensionNamespace: '',
        shortDescription: '',
        description: '',
        name: 'ExtensionName',
        fullName: '',
        tags: ' 12,34, 56  , 789',
        author: '',
        previewIconUrl: '',
        iconUrl: '',
        helpPath: '',
        eventsFunctions: [],
        eventsBasedBehaviors: [],
      });

      eventsFunctionsExtension.unserializeFrom(project, element);
      element.delete();

      expect(eventsFunctionsExtension.getVersion()).toBe('1.0.0');
      expect(eventsFunctionsExtension.getName()).toBe('ExtensionName');

      // Verify that tags were properly converted to an array
      expect(eventsFunctionsExtension.getTags().toJSArray()).toEqual([
        '12',
        '34',
        '56',
        '789',
      ]);

      project.delete();
    });
  });
  describe('gd.EventsBasedBehavior', () => {
    it('can have a name, fullname, description', function () {
      const eventsBasedBehavior = new gd.EventsBasedBehavior();
      eventsBasedBehavior.setName('My name');
      eventsBasedBehavior.setFullName('My descriptive name');
      eventsBasedBehavior.setDescription('My description');
      expect(eventsBasedBehavior.getName()).toBe('My name');
      expect(eventsBasedBehavior.getFullName()).toBe('My descriptive name');
      expect(eventsBasedBehavior.getDescription()).toBe('My description');
    });
  });

  describe('gd.InstructionMetadata', () => {
    it('can have parameters', () => {
      const instructionMetadata = new gd.InstructionMetadata();

      expect(instructionMetadata.getParametersCount()).toBe(0);
      instructionMetadata.addParameter('type', 'label', '', false);
      instructionMetadata.setParameterLongDescription('Blabla');
      expect(instructionMetadata.getParametersCount()).toBe(1);
      expect(instructionMetadata.getParameter(0).getType()).toBe('type');
      expect(instructionMetadata.getParameter(0).getDescription()).toBe(
        'label'
      );
      expect(instructionMetadata.getParameter(0).getLongDescription()).toBe(
        'Blabla'
      );
    });
  });

  describe('gd.ExpressionMetadata', () => {
    it('can have parameters', () => {
      const expressionMetadata = new gd.ExpressionMetadata(
        'number',
        'extensionNamespace',
        'name',
        'fullname',
        'description',
        'group',
        'smallicon'
      );

      expect(expressionMetadata.getReturnType()).toBe('number');
      expect(expressionMetadata.getFullName()).toBe('fullname');
      expect(expressionMetadata.getDescription()).toBe('description');
      expect(expressionMetadata.getGroup()).toBe('group');
      expect(expressionMetadata.getSmallIconFilename()).toBe('smallicon');
      expect(expressionMetadata.isShown()).toBe(true);

      expect(expressionMetadata.getParametersCount()).toBe(0);
      expressionMetadata.addParameter('type', 'label', '', false);
      expressionMetadata.setParameterLongDescription('Blabla');
      expect(expressionMetadata.getParametersCount()).toBe(1);
      expect(expressionMetadata.getParameter(0).getType()).toBe('type');
      expect(expressionMetadata.getParameter(0).getDescription()).toBe('label');
      expect(expressionMetadata.getParameter(0).getLongDescription()).toBe(
        'Blabla'
      );
    });
  });
});
