mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-21 17:45:25 -04:00
dc62aea8d8
- For advanced use cases where it's important to run events on each instance of an object with an ordering, the For Each can now have an ordering: an expression can be written and will be evaluated for each instance separately. The For Each will then execute for each instance in the order of the result of the expression. - It's possible to change the direction (ascending or descending) - It's also possible to give a limit (for example, 1 will allow to pick just the instance with the highest/lowest value for the expression). - Examples are provided in the UI for common use cases (maximum value of a variable, health points, ammo, distance to another object...)
1255 lines
39 KiB
JavaScript
1255 lines
39 KiB
JavaScript
const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js');
|
||
const { makeMinimalGDJSMock } = require('../TestUtils/GDJSMocks');
|
||
const {
|
||
generateCompiledEventsFromSerializedEvents,
|
||
} = require('../TestUtils/CodeGenerationHelpers.js');
|
||
|
||
describe('libGD.js - GDJS ForEach Code Generation integration tests', function () {
|
||
let gd = null;
|
||
beforeAll(async () => {
|
||
gd = await initializeGDevelopJs();
|
||
});
|
||
|
||
// ---- Simple and nested ForEach without orderBy ----
|
||
|
||
it('can generate a simple "for each object" event without orderBy', 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();
|
||
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().get('Counter').getAsNumber()).toBe(
|
||
1 + 2 + 3 + 4
|
||
);
|
||
});
|
||
|
||
it('can generate nested "for each object" events without orderBy', 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();
|
||
const objectALists = new gdjs.Hashtable();
|
||
const myObjectsA = [];
|
||
objectALists.put('MyObjectA', myObjectsA);
|
||
for (let index = 1; index <= 3; index++) {
|
||
const obj = runtimeScene.createObject('MyObjectA');
|
||
obj.getVariables().get('MyVariable').setNumber(index);
|
||
myObjectsA.push(obj);
|
||
}
|
||
const objectBLists = new gdjs.Hashtable();
|
||
const myObjectsB = [];
|
||
objectBLists.put('MyObjectB', myObjectsB);
|
||
for (let index = 1; index <= 2; index++) {
|
||
const obj = runtimeScene.createObject('MyObjectB');
|
||
obj.getVariables().get('MyVariable').setNumber(10 * index);
|
||
myObjectsB.push(obj);
|
||
}
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectALists, objectBLists]);
|
||
|
||
// (1+2+3) * (10+20) = 6*30 = 180
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(180);
|
||
});
|
||
|
||
it('order and limit have no impact when orderBy is not set', function () {
|
||
// Without orderBy, order and limit should be ignored.
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarScene' },
|
||
parameters: ['Counter', '+', '1'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
for (let index = 0; index < 5; index++) {
|
||
myObjects.push(runtimeScene.createObject('MyObject'));
|
||
}
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// All 5 objects iterated
|
||
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(5);
|
||
});
|
||
|
||
// ---- 3 nested ForEach with orderBy ----
|
||
|
||
it('can generate 3 nested "for each" with orderBy, checking object picking', function () {
|
||
// Outer: ForEach on ObjectA ordered by ObjectA.Variable(Priority) asc
|
||
// Middle: ForEach on ObjectB ordered by ObjectB.Variable(Priority) desc
|
||
// Inner: ForEach on ObjectC (no orderBy)
|
||
// Action: Build a string trace to verify exact iteration order.
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'ObjectA',
|
||
orderBy: 'ObjectA.Variable(Priority)',
|
||
order: 'asc',
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'ObjectB',
|
||
orderBy: 'ObjectB.Variable(Priority)',
|
||
order: 'desc',
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'ObjectC',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'Trace',
|
||
'+',
|
||
'ToString(ObjectA.Variable(Priority)) + "," + ToString(ObjectB.Variable(Priority)) + "," + ToString(ObjectC.Variable(Priority)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: {
|
||
ObjectA: 'object',
|
||
ObjectB: 'object',
|
||
ObjectC: 'object',
|
||
},
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
// ObjectA with Priority: 30, 10, 20 -> sorted asc: 10, 20, 30
|
||
const objectALists = new gdjs.Hashtable();
|
||
const objAs = [];
|
||
objectALists.put('ObjectA', objAs);
|
||
[30, 10, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('ObjectA');
|
||
o.getVariables().get('Priority').setNumber(v);
|
||
objAs.push(o);
|
||
});
|
||
|
||
// ObjectB with Priority: 5, 15 -> sorted desc: 15, 5
|
||
const objectBLists = new gdjs.Hashtable();
|
||
const objBs = [];
|
||
objectBLists.put('ObjectB', objBs);
|
||
[5, 15].forEach(v => {
|
||
const o = runtimeScene.createObject('ObjectB');
|
||
o.getVariables().get('Priority').setNumber(v);
|
||
objBs.push(o);
|
||
});
|
||
|
||
// ObjectC with Priority: 1
|
||
const objectCLists = new gdjs.Hashtable();
|
||
const objCs = [];
|
||
objectCLists.put('ObjectC', objCs);
|
||
[1].forEach(v => {
|
||
const o = runtimeScene.createObject('ObjectC');
|
||
o.getVariables().get('Priority').setNumber(v);
|
||
objCs.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [
|
||
objectALists,
|
||
objectBLists,
|
||
objectCLists,
|
||
]);
|
||
|
||
// A sorted asc (10,20,30), B sorted desc (15,5), C unordered (1)
|
||
// Iterations: (10,15,1), (10,5,1), (20,15,1), (20,5,1), (30,15,1), (30,5,1)
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'10,15,1;10,5,1;20,15,1;20,5,1;30,15,1;30,5,1;'
|
||
);
|
||
});
|
||
|
||
// ---- ForEach with orderBy and limit (1, 2, none) for asc and desc ----
|
||
|
||
it('can generate a "for each" with orderBy ascending and limit 1 (pick lowest)', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
orderBy: 'MyObject.Variable(Score)',
|
||
order: 'asc',
|
||
limit: '1',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: ['Trace', '+', 'ToString(MyObject.Variable(Score)) + ";"'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
[50, 10, 30, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('MyObject');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
myObjects.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// Ascending + limit 1 → picks the object with lowest Score = 10
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'10;'
|
||
);
|
||
});
|
||
|
||
it('can generate a "for each" with orderBy descending and limit 1 (pick highest)', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
orderBy: 'MyObject.Variable(Score)',
|
||
order: 'desc',
|
||
limit: '1',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: ['Trace', '+', 'ToString(MyObject.Variable(Score)) + ";"'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
[50, 10, 30, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('MyObject');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
myObjects.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// Descending + limit 1 → picks the object with highest Score = 50
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'50;'
|
||
);
|
||
});
|
||
|
||
it('can generate a "for each" with orderBy ascending and limit 2', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
orderBy: 'MyObject.Variable(Score)',
|
||
order: 'asc',
|
||
limit: '2',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: ['Trace', '+', 'ToString(MyObject.Variable(Score)) + ";"'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
[50, 10, 30, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('MyObject');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
myObjects.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// Ascending + limit 2 → picks 10, then 20
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'10;20;'
|
||
);
|
||
});
|
||
|
||
it('can generate a "for each" with orderBy descending and limit 2', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
orderBy: 'MyObject.Variable(Score)',
|
||
order: 'desc',
|
||
limit: '2',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: ['Trace', '+', 'ToString(MyObject.Variable(Score)) + ";"'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
[50, 10, 30, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('MyObject');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
myObjects.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// Descending + limit 2 → picks 50, then 30
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'50;30;'
|
||
);
|
||
});
|
||
|
||
it('can generate a "for each" with orderBy ascending and no limit', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
orderBy: 'MyObject.Variable(Score)',
|
||
order: 'asc',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: ['Trace', '+', 'ToString(MyObject.Variable(Score)) + ";"'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
[50, 10, 30, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('MyObject');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
myObjects.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// Ascending, no limit → iterates all scores in order: 10, 20, 30, 50
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'10;20;30;50;'
|
||
);
|
||
});
|
||
|
||
it('can generate a "for each" with orderBy descending and no limit', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
orderBy: 'MyObject.Variable(Score)',
|
||
order: 'desc',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: ['Trace', '+', 'ToString(MyObject.Variable(Score)) + ";"'],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
[50, 10, 30, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('MyObject');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
myObjects.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// Descending, no limit → iterates all scores in order: 50, 30, 20, 10
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'50;30;20;10;'
|
||
);
|
||
});
|
||
|
||
// ---- ForEach with orderBy with local variables and loop index ----
|
||
|
||
it('can generate a "for each" with orderBy, local variables and loop index', function () {
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
orderBy: 'MyObject.Variable(Score)',
|
||
order: 'asc',
|
||
variables: [
|
||
{ name: 'LoopIndex', type: 'number', value: 0 },
|
||
{ name: 'Multiplier', type: 'number', value: 10 },
|
||
],
|
||
loopIndexVariable: 'LoopIndex',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'Trace',
|
||
'+',
|
||
'ToString(MyObject.Variable(Score)) + "*" + ToString(Multiplier) + "+" + ToString(LoopIndex) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
[30, 10, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('MyObject');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
myObjects.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// Objects sorted asc by Score: 10, 20, 30
|
||
// Loop indices: 0, 1, 2. Multiplier is always 10.
|
||
// Trace = "10*10+0;20*10+1;30*10+2;"
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'10*10+0;20*10+1;30*10+2;'
|
||
);
|
||
expect(runtimeScene.getVariables().has('LoopIndex')).toBe(false);
|
||
expect(runtimeScene.getVariables().has('Multiplier')).toBe(false);
|
||
});
|
||
|
||
// ---- ForEach orderBy/limit object picking context tests ----
|
||
|
||
it('orderBy correctly resolves an unpicked object referenced in the expression', function () {
|
||
// orderBy uses "Modifier.Variable(Factor) * Coins.Variable(Score)".
|
||
// Coins is iterated (one at a time in sortContext).
|
||
// Modifier is NOT picked by any condition or for-each, so all instances
|
||
// are available; the expression uses the first instance's Factor value.
|
||
// With Factor=-1, the sort is effectively reversed, proving Modifier is used.
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'Coins',
|
||
orderBy: 'Modifier.Variable(Factor) * Coins.Variable(Score)',
|
||
order: 'asc',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'Trace',
|
||
'+',
|
||
'ToString(Coins.Variable(Score)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { Coins: 'object', Modifier: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
const coinsLists = new gdjs.Hashtable();
|
||
const coins = [];
|
||
coinsLists.put('Coins', coins);
|
||
[30, 10, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('Coins');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
coins.push(o);
|
||
});
|
||
|
||
// Modifier with Factor=-1 (single unpicked instance)
|
||
const modLists = new gdjs.Hashtable();
|
||
const mods = [];
|
||
modLists.put('Modifier', mods);
|
||
const mod = runtimeScene.createObject('Modifier');
|
||
mod.getVariables().get('Factor').setNumber(-1);
|
||
mods.push(mod);
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [coinsLists, modLists]);
|
||
|
||
// Sort keys: -1*30=-30, -1*10=-10, -1*20=-20
|
||
// Asc: -30, -20, -10 → Coins 30, 20, 10 (reversed from normal ascending)
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'30;20;10;'
|
||
);
|
||
});
|
||
|
||
it('nested for-each orderBy correctly accesses the parent-iterated object', function () {
|
||
// Outer: ForEach Player ordered by Priority asc
|
||
// Inner: ForEach Coins ordered by "Player.Variable(Sign) * Coins.Variable(Score)" asc
|
||
// Player is picked by the outer for-each (single instance per iteration).
|
||
// Player(Sign=1) → Coins in normal ascending order.
|
||
// Player(Sign=-1) → Coins in reversed order.
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'Player',
|
||
orderBy: 'Player.Variable(Priority)',
|
||
order: 'asc',
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'Coins',
|
||
orderBy: 'Player.Variable(Sign) * Coins.Variable(Score)',
|
||
order: 'asc',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'Trace',
|
||
'+',
|
||
'ToString(Player.Variable(Priority)) + ":" + ToString(Coins.Variable(Score)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { Player: 'object', Coins: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
const playerLists = new gdjs.Hashtable();
|
||
const players = [];
|
||
playerLists.put('Player', players);
|
||
// Priority=2, Sign=-1
|
||
const p1 = runtimeScene.createObject('Player');
|
||
p1.getVariables().get('Priority').setNumber(2);
|
||
p1.getVariables().get('Sign').setNumber(-1);
|
||
players.push(p1);
|
||
// Priority=1, Sign=1
|
||
const p2 = runtimeScene.createObject('Player');
|
||
p2.getVariables().get('Priority').setNumber(1);
|
||
p2.getVariables().get('Sign').setNumber(1);
|
||
players.push(p2);
|
||
|
||
const coinsLists = new gdjs.Hashtable();
|
||
const coinsList = [];
|
||
coinsLists.put('Coins', coinsList);
|
||
[30, 10, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('Coins');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
coinsList.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [playerLists, coinsLists]);
|
||
|
||
// Outer: Players sorted by Priority asc → P2(1,Sign=1), P1(2,Sign=-1)
|
||
// For P2(Sign=1): keys 1*30=30, 1*10=10, 1*20=20 → asc: 10,20,30
|
||
// For P1(Sign=-1): keys -30, -10, -20 → asc: -30,-20,-10 → Coins 30,20,10
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'1:10;1:20;1:30;2:30;2:20;2:10;'
|
||
);
|
||
});
|
||
|
||
it('limit expression correctly resolves an unpicked object', function () {
|
||
// ForEach Coins ordered by Score asc, limit = Config.Variable(Max).
|
||
// Config is NOT picked, so all instances are available and the expression
|
||
// uses the first instance's Max value.
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'Coins',
|
||
orderBy: 'Coins.Variable(Score)',
|
||
order: 'asc',
|
||
limit: 'Config.Variable(Max)',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'Trace',
|
||
'+',
|
||
'ToString(Coins.Variable(Score)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { Coins: 'object', Config: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
const coinsLists = new gdjs.Hashtable();
|
||
const coins = [];
|
||
coinsLists.put('Coins', coins);
|
||
[50, 10, 30, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('Coins');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
coins.push(o);
|
||
});
|
||
|
||
// Config with Max=2 (single unpicked instance)
|
||
const configLists = new gdjs.Hashtable();
|
||
const configs = [];
|
||
configLists.put('Config', configs);
|
||
const cfg = runtimeScene.createObject('Config');
|
||
cfg.getVariables().get('Max').setNumber(2);
|
||
configs.push(cfg);
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [coinsLists, configLists]);
|
||
|
||
// Sorted asc: 10, 20, 30, 50; limit Config.Max=2 → 10, 20
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'10;20;'
|
||
);
|
||
});
|
||
|
||
it('limit expression correctly resolves a parent-picked object, varying per iteration', function () {
|
||
// Outer: ForEach Player ordered by Priority asc
|
||
// Inner: ForEach Coins ordered by Score asc, limit = Player.Variable(MaxCoins)
|
||
// Player is picked by the outer for-each, so each iteration uses a
|
||
// different limit value.
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'Player',
|
||
orderBy: 'Player.Variable(Priority)',
|
||
order: 'asc',
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'Coins',
|
||
orderBy: 'Coins.Variable(Score)',
|
||
order: 'asc',
|
||
limit: 'Player.Variable(MaxCoins)',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'Trace',
|
||
'+',
|
||
'ToString(Player.Variable(Priority)) + ":" + ToString(Coins.Variable(Score)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { Player: 'object', Coins: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
const playerLists = new gdjs.Hashtable();
|
||
const players = [];
|
||
playerLists.put('Player', players);
|
||
// Priority=2, MaxCoins=3
|
||
const pa = runtimeScene.createObject('Player');
|
||
pa.getVariables().get('Priority').setNumber(2);
|
||
pa.getVariables().get('MaxCoins').setNumber(3);
|
||
players.push(pa);
|
||
// Priority=1, MaxCoins=1
|
||
const pb = runtimeScene.createObject('Player');
|
||
pb.getVariables().get('Priority').setNumber(1);
|
||
pb.getVariables().get('MaxCoins').setNumber(1);
|
||
players.push(pb);
|
||
|
||
const coinsLists = new gdjs.Hashtable();
|
||
const coins = [];
|
||
coinsLists.put('Coins', coins);
|
||
[50, 10, 30, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('Coins');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
coins.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [playerLists, coinsLists]);
|
||
|
||
// Outer: Players sorted by Priority asc → PB(1,MaxCoins=1), PA(2,MaxCoins=3)
|
||
// For PB(MaxCoins=1): Coins asc: 10,20,30,50; limit 1 → [10]
|
||
// For PA(MaxCoins=3): Coins asc: 10,20,30,50; limit 3 → [10,20,30]
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'1:10;2:10;2:20;2:30;'
|
||
);
|
||
});
|
||
|
||
it('condition-filtered objects are correctly used in nested for-each with orderBy', function () {
|
||
// Outer: ForEach Coins ordered by Score asc, limit 2
|
||
// Standard event with condition: Diamond.Variable(Active) = 1 (filters Diamonds)
|
||
// Inner: ForEach Diamond ordered by Priority asc
|
||
// trace Coins.Score + ":" + Diamond.Priority
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'Coins',
|
||
orderBy: 'Coins.Variable(Score)',
|
||
order: 'asc',
|
||
limit: '2',
|
||
conditions: [],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::Standard',
|
||
conditions: [
|
||
{
|
||
type: { value: 'VarObjet' },
|
||
parameters: ['Diamond', 'Active', '=', '1'],
|
||
},
|
||
],
|
||
actions: [],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'Diamond',
|
||
orderBy: 'Diamond.Variable(Priority)',
|
||
order: 'asc',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'Trace',
|
||
'+',
|
||
'ToString(Coins.Variable(Score)) + ":" + ToString(Diamond.Variable(Priority)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { Coins: 'object', Diamond: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
const coinsLists = new gdjs.Hashtable();
|
||
const coins = [];
|
||
coinsLists.put('Coins', coins);
|
||
[30, 10, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('Coins');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
coins.push(o);
|
||
});
|
||
|
||
// Diamond: Priority=3/Active=1, Priority=1/Active=0, Priority=2/Active=1
|
||
const diamondLists = new gdjs.Hashtable();
|
||
const diamonds = [];
|
||
diamondLists.put('Diamond', diamonds);
|
||
[[3, 1], [1, 0], [2, 1]].forEach(([prio, active]) => {
|
||
const o = runtimeScene.createObject('Diamond');
|
||
o.getVariables().get('Priority').setNumber(prio);
|
||
o.getVariables().get('Active').setNumber(active);
|
||
diamonds.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [coinsLists, diamondLists]);
|
||
|
||
// Outer: Coins sorted asc: 10, 20, 30; limit 2 → Coins 10, 20
|
||
// Condition picks Diamonds with Active=1: Priority=3, Priority=2
|
||
// Inner: Diamonds sorted asc by Priority → 2, 3
|
||
// For Coins(10): "10:2;10:3;"
|
||
// For Coins(20): "20:2;20:3;"
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'10:2;10:3;20:2;20:3;'
|
||
);
|
||
});
|
||
|
||
it('limit of 0 results in zero iterations when orderBy is set', function () {
|
||
// A limit of 0 means "iterate 0 objects" — no iterations should happen.
|
||
// Only an empty/unspecified limit means "no limit".
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
orderBy: 'MyObject.Variable(Score)',
|
||
order: 'asc',
|
||
limit: '0',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'Trace',
|
||
'+',
|
||
'ToString(MyObject.Variable(Score)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
[50, 10, 30, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('MyObject');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
myObjects.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// Limit 0 → zero iterations, trace stays empty
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe('');
|
||
});
|
||
|
||
it('negative limit does not truncate when orderBy is set', function () {
|
||
// A negative limit is treated as "no limit" (all objects iterated).
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'MyObject',
|
||
orderBy: 'MyObject.Variable(Score)',
|
||
order: 'asc',
|
||
limit: '-1',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'Trace',
|
||
'+',
|
||
'ToString(MyObject.Variable(Score)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { MyObject: 'object' },
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
const objectLists = new gdjs.Hashtable();
|
||
const myObjects = [];
|
||
objectLists.put('MyObject', myObjects);
|
||
[50, 10, 30, 20].forEach(v => {
|
||
const o = runtimeScene.createObject('MyObject');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
myObjects.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('Trace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
|
||
|
||
// Negative limit → no truncation, all objects iterated in sorted order
|
||
expect(runtimeScene.getVariables().get('Trace').getAsString()).toBe(
|
||
'10;20;30;50;'
|
||
);
|
||
});
|
||
|
||
// ---- 3 nested ForEach with orderBy on a group then on objects of this group ----
|
||
|
||
it('can generate 3 nested "for each" with orderBy on a group then on objects of that group', function () {
|
||
// Outer: ForEach on AllObjects (group: ObjX, ObjY) ordered by Score asc
|
||
// Middle: ForEach on IndependentObj ordered by Val desc
|
||
// Inner: ForEach on ThirdObj ordered by V asc
|
||
// Each level appends to its own string trace to verify exact ordering.
|
||
const serializerElement = gd.Serializer.fromJSObject([
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'AllObjects',
|
||
orderBy: 'AllObjects.Variable(Score)',
|
||
order: 'asc',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'OuterTrace',
|
||
'+',
|
||
'ToString(AllObjects.Variable(Score)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'IndependentObj',
|
||
orderBy: 'IndependentObj.Variable(Val)',
|
||
order: 'desc',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'InnerTrace',
|
||
'+',
|
||
'ToString(AllObjects.Variable(Score)) + "+" + ToString(IndependentObj.Variable(Val)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [
|
||
{
|
||
type: 'BuiltinCommonInstructions::ForEach',
|
||
object: 'ThirdObj',
|
||
orderBy: 'ThirdObj.Variable(V)',
|
||
order: 'asc',
|
||
conditions: [],
|
||
actions: [
|
||
{
|
||
type: { value: 'ModVarSceneTxt' },
|
||
parameters: [
|
||
'DeepTrace',
|
||
'+',
|
||
'ToString(AllObjects.Variable(Score)) + "+" + ToString(IndependentObj.Variable(Val)) + "+" + ToString(ThirdObj.Variable(V)) + ";"',
|
||
],
|
||
},
|
||
],
|
||
events: [],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
|
||
gd,
|
||
serializerElement,
|
||
{
|
||
parameterTypes: { ObjX: 'object', ObjY: 'object', IndependentObj: 'object', ThirdObj: 'object' },
|
||
groups: {
|
||
AllObjects: ['ObjX', 'ObjY'],
|
||
},
|
||
logCode: false,
|
||
}
|
||
);
|
||
|
||
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
|
||
|
||
// ObjX with Scores: 5, 15
|
||
const objXLists = new gdjs.Hashtable();
|
||
const objXs = [];
|
||
objXLists.put('ObjX', objXs);
|
||
[5, 15].forEach(v => {
|
||
const o = runtimeScene.createObject('ObjX');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
objXs.push(o);
|
||
});
|
||
|
||
// ObjY with Scores: 100, 200
|
||
const objYLists = new gdjs.Hashtable();
|
||
const objYs = [];
|
||
objYLists.put('ObjY', objYs);
|
||
[100, 200].forEach(v => {
|
||
const o = runtimeScene.createObject('ObjY');
|
||
o.getVariables().get('Score').setNumber(v);
|
||
objYs.push(o);
|
||
});
|
||
|
||
// IndependentObj with Val: 1000, 2000
|
||
const indepLists = new gdjs.Hashtable();
|
||
const indepObjs = [];
|
||
indepLists.put('IndependentObj', indepObjs);
|
||
[1000, 2000].forEach(v => {
|
||
const o = runtimeScene.createObject('IndependentObj');
|
||
o.getVariables().get('Val').setNumber(v);
|
||
indepObjs.push(o);
|
||
});
|
||
|
||
// ThirdObj with V: 7
|
||
const thirdLists = new gdjs.Hashtable();
|
||
const thirdObjs = [];
|
||
thirdLists.put('ThirdObj', thirdObjs);
|
||
[7].forEach(v => {
|
||
const o = runtimeScene.createObject('ThirdObj');
|
||
o.getVariables().get('V').setNumber(v);
|
||
thirdObjs.push(o);
|
||
});
|
||
|
||
runtimeScene.getVariables().get('OuterTrace').setString('');
|
||
runtimeScene.getVariables().get('InnerTrace').setString('');
|
||
runtimeScene.getVariables().get('DeepTrace').setString('');
|
||
|
||
runCompiledEvents(gdjs, runtimeScene, [objXLists, objYLists, indepLists, thirdLists]);
|
||
|
||
// Outer ForEach: AllObjects (group) sorted asc by Score:
|
||
// ObjX(5), ObjX(15), ObjY(100), ObjY(200)
|
||
expect(runtimeScene.getVariables().get('OuterTrace').getAsString()).toBe(
|
||
'5;15;100;200;'
|
||
);
|
||
|
||
// Middle ForEach on IndependentObj desc (2000, 1000) for each outer:
|
||
// 4 outer × 2 inner = 8 iterations
|
||
expect(runtimeScene.getVariables().get('InnerTrace').getAsString()).toBe(
|
||
'5+2000;5+1000;15+2000;15+1000;100+2000;100+1000;200+2000;200+1000;'
|
||
);
|
||
|
||
// Deep ForEach on ThirdObj asc (7) for each of 8 middle iterations:
|
||
expect(runtimeScene.getVariables().get('DeepTrace').getAsString()).toBe(
|
||
'5+2000+7;5+1000+7;15+2000+7;15+1000+7;100+2000+7;100+1000+7;200+2000+7;200+1000+7;'
|
||
);
|
||
});
|
||
});
|