Files
GDevelop/GDevelop.js/__tests__/GDJSBasicCodeGenerationIntegrationTests.js
T
2023-05-23 14:22:39 +02:00

860 lines
26 KiB
JavaScript

const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js');
const { makeMinimalGDJSMock } = require('../TestUtils/GDJSMocks');
const {
generateCompiledEventsForEventsFunction,
generateCompiledEventsFromSerializedEvents,
} = require('../TestUtils/CodeGenerationHelpers.js');
/**
* Helper generating an event, ready to be unserialized, adding 1 to
* "TestVariable" of the specified object (or object group).
*/
const makeAddOneToObjectTestVariableEvent = (objectName) => ({
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarObjet' },
parameters: [objectName, 'TestVariable', '+', '1'],
},
],
events: [],
});
describe('libGD.js - GDJS Code Generation integration tests', function () {
let gd = null;
beforeAll((done) =>
initializeGDevelopJs().then((module) => {
gd = module;
done();
})
);
it('can generate a While event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '=', '0'],
},
],
events: [],
},
{
infiniteLoopWarning: true,
type: 'BuiltinCommonInstructions::While',
whileConditions: [
{
type: { value: 'VarScene' },
parameters: ['Counter', '<', '4'],
},
],
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', '1'],
},
],
events: [],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(4);
});
it('can generate nested While events', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '=', '0'],
},
],
events: [],
},
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['CellY', '=', '0'],
},
],
events: [],
},
{
infiniteLoopWarning: true,
type: 'BuiltinCommonInstructions::While',
whileConditions: [
{
type: { value: 'VarScene' },
parameters: ['CellY', '<', '4'],
},
],
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['CellX', '=', '0'],
},
],
events: [
{
infiniteLoopWarning: true,
type: 'BuiltinCommonInstructions::While',
whileConditions: [
{
type: { value: 'VarScene' },
parameters: ['CellX', '<', '5'],
},
],
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['CellX', '+', '1'],
},
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', '1'],
},
],
events: [],
},
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['CellY', '+', '1'],
},
],
events: [],
},
],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(20);
});
it('can generate a Repeat event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Repeat',
repeatExpression: '4',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', '1'],
},
],
events: [],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(4);
});
it('can generate a nested Repeat event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Repeat',
repeatExpression: '4',
conditions: [],
actions: [],
events: [
{
type: 'BuiltinCommonInstructions::Repeat',
repeatExpression: '5',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', '1'],
},
],
events: [],
},
],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(20);
});
it('can generate a "for each object" event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::ForEach',
object: 'MyObject',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', 'MyObject.Variable(MyVariable)'],
},
],
events: [],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement,
{
parameterTypes: {
MyObject: 'object',
},
logCode: false,
}
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
// Create 4 objects with variable values from 1 to 4.
const objectLists = new gdjs.Hashtable();
const myObjects = [];
objectLists.put('MyObject', myObjects);
for (let index = 1; index <= 4; index++) {
const myObject = runtimeScene.createObject('MyObject');
myObject.getVariables().get('MyVariable').setNumber(index);
myObjects.push(myObject);
}
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
1 + 2 + 3 + 4
);
});
it('can generate a nested "for each object" event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::ForEach',
object: 'MyObjectA',
conditions: [],
actions: [],
events: [
{
type: 'BuiltinCommonInstructions::ForEach',
object: 'MyObjectB',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'Counter',
'+',
'MyObjectA.Variable(MyVariable) * MyObjectB.Variable(MyVariable)',
],
},
],
events: [],
},
],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement,
{
parameterTypes: {
MyObjectA: 'object',
MyObjectB: 'object',
},
logCode: false,
}
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
// Create 4 objects with variable values from 1 to 4.
const objectALists = new gdjs.Hashtable();
const myObjectsA = [];
objectALists.put('MyObjectA', myObjectsA);
for (let index = 1; index <= 4; index++) {
const myObjectA = runtimeScene.createObject('MyObjectA');
myObjectA.getVariables().get('MyVariable').setNumber(index);
myObjectsA.push(myObjectA);
}
// Create 3 objects with variable values from 5 to 7.
const objectBLists = new gdjs.Hashtable();
const myObjectsB = [];
objectBLists.put('MyObjectB', myObjectsB);
for (let index = 1; index <= 3; index++) {
const myObjectB = runtimeScene.createObject('MyObjectB');
myObjectB
.getVariables()
.get('MyVariable')
.setNumber(4 + index);
myObjectsB.push(myObjectB);
}
runCompiledEvents(gdjs, runtimeScene, [objectALists, objectBLists]);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
(1 + 2 + 3 + 4) * (5 + 6 + 7)
);
});
it('can generate a "for each child variable" event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::ForEachChildVariable',
iterableVariableName: 'MyVariable',
valueIteratorVariableName: 'child',
keyIteratorVariableName: '',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', 'Variable(child)'],
},
],
events: [],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement,
{
logCode: false,
}
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
// Create 4 child-variables with values from 1 to 4.
const myVariable = runtimeScene.getVariables().get('MyVariable');
for (let index = 1; index <= 4; index++) {
myVariable.getChild('Child' + index).setNumber(index);
}
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
1 + 2 + 3 + 4
);
});
it('can generate a nested "for each child variable" event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::ForEachChildVariable',
iterableVariableName: 'MyVariableA',
valueIteratorVariableName: 'childA',
keyIteratorVariableName: '',
conditions: [],
actions: [],
events: [
{
type: 'BuiltinCommonInstructions::ForEachChildVariable',
iterableVariableName: 'MyVariableB',
valueIteratorVariableName: 'childB',
keyIteratorVariableName: '',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'Counter',
'+',
'Variable(childA) * Variable(childB)',
],
},
],
events: [],
},
],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement,
{
logCode: false,
}
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
// Create 4 child-variables with values from 1 to 4.
const myVariableA = runtimeScene.getVariables().get('MyVariableA');
for (let index = 1; index <= 4; index++) {
myVariableA.getChild('Child' + index).setNumber(index);
}
// Create 4 child-variables with values from 5 to 7.
const myVariableB = runtimeScene.getVariables().get('MyVariableB');
for (let index = 1; index <= 3; index++) {
myVariableB.getChild('Child' + index).setNumber(4 + index);
}
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
(1 + 2 + 3 + 4) * (5 + 6 + 7)
);
});
it('generates a working function creating objects', function () {
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'Create' },
parameters: ['', 'MyObjectA', '0', '0', ''],
},
],
events: [makeAddOneToObjectTestVariableEvent('MyObjectA')],
},
makeAddOneToObjectTestVariableEvent('MyObjectA'),
makeAddOneToObjectTestVariableEvent('MyObjectB'),
]);
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction
.getEvents()
.unserializeFrom(project, eventsSerializerElement);
// Add an object parameter to the function:
const objectParameter = new gd.ParameterMetadata();
objectParameter.setType('object');
objectParameter.setName('MyObjectA');
eventsFunction.getParameters().push_back(objectParameter);
objectParameter.setType('object');
objectParameter.setName('MyObjectB');
eventsFunction.getParameters().push_back(objectParameter);
objectParameter.delete();
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runtimeScene.getOnceTriggers().startNewFrame();
const emptyMyObjectALists = gdjs.Hashtable.newFrom({ MyObjectA: [] });
const myObjectB = runtimeScene.createObject('MyObjectB');
const myObjectBLists = gdjs.Hashtable.newFrom({ MyObjectB: [myObjectB] });
runCompiledEvents(gdjs, runtimeScene, [
emptyMyObjectALists,
myObjectBLists,
]);
// Check that the object was created and its variable was incremented twice.
expect(runtimeScene.getObjects('MyObjectA')).toHaveLength(1);
const firstMyObjectA = runtimeScene.getObjects('MyObjectA')[0];
expect(firstMyObjectA.getVariables().has('TestVariable')).toBe(true);
expect(
firstMyObjectA.getVariables().get('TestVariable').getAsNumber()
).toBe(2);
// Object B has nothing to do with the "Create", its variable is just incremented once.
expect(myObjectB.getVariables().get('TestVariable').getAsNumber()).toBe(1);
// Check that if the function is called with the first MyObjectA as parameter, it won't change
// the way a new object is created...
const myObjectALists = gdjs.Hashtable.newFrom({
MyObjectA: [firstMyObjectA],
});
runCompiledEvents(gdjs, runtimeScene, [myObjectALists, myObjectBLists]);
expect(runtimeScene.getObjects('MyObjectA')).toHaveLength(2);
const secondMyObjectA = runtimeScene.getObjects('MyObjectA')[1];
expect(secondMyObjectA.getVariables().has('TestVariable')).toBe(true);
expect(
secondMyObjectA.getVariables().get('TestVariable').getAsNumber()
).toBe(2);
// ... and the first, "already created" MyObjectA is only picked by the second event.
// The first event with the "Create" will only pick the "just created" object.
expect(
firstMyObjectA.getVariables().get('TestVariable').getAsNumber()
).toBe(3);
// Object B has nothing to do with the "Create", its variable is just incremented again.
expect(myObjectB.getVariables().get('TestVariable').getAsNumber()).toBe(2);
eventsFunction.delete();
project.delete();
});
it('generates working functions with groups', function () {
// Create events that increment twice the variable of a group, and
// only once for the 3rd parameter.
const eventsSerializerElement = gd.Serializer.fromJSObject([
makeAddOneToObjectTestVariableEvent('MyObjectGroup'),
makeAddOneToObjectTestVariableEvent('MyObjectGroup'),
makeAddOneToObjectTestVariableEvent('ObjectParam3'),
]);
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction
.getEvents()
.unserializeFrom(project, eventsSerializerElement);
const group = eventsFunction
.getObjectGroups()
.insertNew('MyObjectGroup', 0);
group.setName('MyObjectGroup');
group.addObject('ObjectParam1');
group.addObject('ObjectParam2');
const objectParameter = new gd.ParameterMetadata();
objectParameter.setType('object');
objectParameter.setName('ObjectParam1');
eventsFunction.getParameters().push_back(objectParameter);
objectParameter.setType('object');
objectParameter.setName('ObjectParam2');
eventsFunction.getParameters().push_back(objectParameter);
objectParameter.setType('object');
objectParameter.setName('ObjectParam3');
eventsFunction.getParameters().push_back(objectParameter);
objectParameter.delete();
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
// Check that when running the function, the group works in the function.
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runtimeScene.getOnceTriggers().startNewFrame();
const myObjectA = runtimeScene.createObject('MyObjectA');
const myObjectB = runtimeScene.createObject('MyObjectB');
const myObjectC = runtimeScene.createObject('MyObjectC');
const myObjectD = runtimeScene.createObject('MyObjectD');
const objectParam1Lists = gdjs.Hashtable.newFrom({
// The first parameter is itself a collection of multiple objects
// with different names.
MyObjectA: [myObjectA],
MyObjectB: [myObjectB],
});
const objectParam2Lists = gdjs.Hashtable.newFrom({
MyObjectC: [myObjectC],
});
const objectParam3Lists = gdjs.Hashtable.newFrom({
MyObjectD: [myObjectD],
});
runCompiledEvents(gdjs, runtimeScene, [
objectParam1Lists,
objectParam2Lists,
objectParam3Lists,
]);
expect(runtimeScene.getObjects('MyObjectA')).toHaveLength(1);
const firstMyObjectA = runtimeScene.getObjects('MyObjectA')[0];
expect(
firstMyObjectA.getVariables().get('TestVariable').getAsNumber()
).toBe(2);
expect(runtimeScene.getObjects('MyObjectB')).toHaveLength(1);
const firstMyObjectB = runtimeScene.getObjects('MyObjectB')[0];
expect(
firstMyObjectB.getVariables().get('TestVariable').getAsNumber()
).toBe(2);
expect(runtimeScene.getObjects('MyObjectC')).toHaveLength(1);
const firstMyObjectC = runtimeScene.getObjects('MyObjectC')[0];
expect(
firstMyObjectC.getVariables().get('TestVariable').getAsNumber()
).toBe(2);
expect(runtimeScene.getObjects('MyObjectD')).toHaveLength(1);
const firstMyObjectD = runtimeScene.getObjects('MyObjectD')[0];
expect(
firstMyObjectD.getVariables().get('TestVariable').getAsNumber()
).toBe(1);
eventsFunction.delete();
project.delete();
});
it('generates a working function with BuiltinCommonInstructions::Once', function () {
// Event to create an object, then add
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
value: 'BuiltinCommonInstructions::Once',
},
parameters: [],
},
],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
},
],
events: [],
},
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
value: 'BuiltinCommonInstructions::Once',
},
parameters: [],
},
],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
},
],
events: [],
},
]);
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction
.getEvents()
.unserializeFrom(project, eventsSerializerElement);
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runtimeScene.getOnceTriggers().startNewFrame();
runCompiledEvents(gdjs, runtimeScene, []);
runtimeScene.getOnceTriggers().startNewFrame();
runCompiledEvents(gdjs, runtimeScene, []);
// Check that after running the compiled events twice, actions were
// executed only twice.
expect(runtimeScene.getVariables().has('SuccessVariable')).toBe(true);
expect(
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
).toBe(2);
eventsFunction.delete();
project.delete();
});
it('generates a working function with BuiltinCommonInstructions::Once with stable IDs across compilation', function () {
// Create events using the Trigger Once condition.
const eventsSerializerElement = gd.Serializer.fromJSON(
JSON.stringify([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
value: 'BuiltinCommonInstructions::Once',
},
parameters: [],
},
],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
},
],
events: [],
},
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [
{
type: {
value: 'BuiltinCommonInstructions::Once',
},
parameters: [],
},
],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
},
],
events: [],
},
])
);
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction
.getEvents()
.unserializeFrom(project, eventsSerializerElement);
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runtimeScene.getOnceTriggers().startNewFrame();
runCompiledEvents(gdjs, runtimeScene, []);
runtimeScene.getOnceTriggers().startNewFrame();
runCompiledEvents(gdjs, runtimeScene, []);
// Check that after running the compiled events twice, actions were
// executed only twice.
expect(runtimeScene.getVariables().has('SuccessVariable')).toBe(true);
expect(
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
).toBe(2);
// Check that compiling again the events will result in stable ids for Trigger Once conditions
// (trigger once should not be launched again), even after a copy of the events function.
const eventsFunctionCopy = eventsFunction.clone();
const runCompiledEvents2 = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunctionCopy
);
runtimeScene.getOnceTriggers().startNewFrame();
runCompiledEvents2(gdjs, runtimeScene, []);
runtimeScene.getOnceTriggers().startNewFrame();
runCompiledEvents2(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('SuccessVariable')).toBe(true);
expect(
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
).toBe(2);
eventsFunction.delete();
eventsFunctionCopy.delete();
project.delete();
});
it('generates a lifecycle function that registers itself, and unregister itself if hot-reloaded', function () {
// Event to create an object, then add
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['SuccessVariable', '+', '1'],
},
],
events: [],
},
]);
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunction = new gd.EventsFunction();
eventsFunction.setName('onScenePreEvents');
eventsFunction
.getEvents()
.unserializeFrom(project, eventsSerializerElement);
const runCompiledEvents = generateCompiledEventsForEventsFunction(
gd,
project,
eventsFunction
);
const { gdjs, runtimeScene, mocks } = makeMinimalGDJSMock();
runCompiledEvents(
gdjs,
runtimeScene /*, Don't pass arguments to not run the function. */
);
mocks.runRuntimeScenePreEventsCallbacks();
expect(
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
).toBe(1);
mocks.runRuntimeScenePreEventsCallbacks();
expect(
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
).toBe(2);
// Simulate a hot reloading by recompiling the function and running it again.
const runHotReloadedCompiledEvents =
generateCompiledEventsForEventsFunction(gd, project, eventsFunction);
runHotReloadedCompiledEvents(
gdjs,
runtimeScene /*, Don't pass arguments to not run the function. */
);
// Ensure that when we call the callbacks, it's called only once (not registered twice).
mocks.runRuntimeScenePreEventsCallbacks();
expect(
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
).toBe(3);
mocks.runRuntimeScenePreEventsCallbacks();
expect(
runtimeScene.getVariables().get('SuccessVariable').getAsNumber()
).toBe(4);
eventsFunction.delete();
project.delete();
});
});