Add integration test for objects created via a behavior action parameter (#8576)

Verify that a behavior's custom action whose events Create the object
passed as parameter (FireBullet-style) leads to the newly created
instances being picked in the calling scope, while a previously created
instance of the same object remains untouched.

Don't show in changelog
This commit is contained in:
Florian Rival
2026-05-07 19:00:52 +02:00
committed by GitHub
parent 230657d076
commit d52d42d3b4
@@ -0,0 +1,210 @@
const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js');
const { makeMinimalGDJSMock } = require('../TestUtils/GDJSMocks');
const {
generateCompiledEventsForEventsBasedBehavior,
generateCompiledEventsForLayout,
} = require('../TestUtils/CodeGenerationHelpers.js');
describe('libGD.js - GDJS Advanced Behavior Code Generation integration tests', function () {
let gd = null;
beforeAll(async () => {
gd = await initializeGDevelopJs();
});
describe('Object parameter creation (FireBullet-like behavior)', () => {
it('Picks only the instances created by a behavior action via an object parameter', () => {
// A behavior MyBehavior has a custom action MyAction that takes an
// object as parameter. The action's events use Create twice on the
// object passed as parameter, mimicking a FireBullet behavior.
//
// The scene contains two objects: Bullet (the parameter type) and
// Cannon (the object holding the behavior). The scene events first
// create one Bullet directly (without going through the behavior),
// and in a subsequent event call MyBehavior::MyAction(Bullet) on the
// Cannon, then increment a Counter on the picked Bullet. Only the
// two newly created Bullets must be picked by this increment - not
// the one created in the previous scene event.
const project = new gd.ProjectHelper.createNewGDJSProject();
const eventsFunctionsExtension = project.insertNewEventsFunctionsExtension(
'MyExtension',
0
);
const eventsBasedBehavior = eventsFunctionsExtension
.getEventsBasedBehaviors()
.insertNew('MyBehavior', 0);
// Behavior's MyAction creates the parameter object twice.
const behaviorEventsSerializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'Create' },
parameters: ['', 'MyObjectParam', '0', '0', ''],
},
{
type: { value: 'Create' },
parameters: ['', 'MyObjectParam', '0', '0', ''],
},
],
},
]);
const behaviorEventsFunction = eventsBasedBehavior
.getEventsFunctions()
.insertNewEventsFunction('MyAction', 0);
behaviorEventsFunction
.getEvents()
.unserializeFrom(project, behaviorEventsSerializerElement);
behaviorEventsSerializerElement.delete();
gd.WholeProjectRefactorer.ensureBehaviorEventsFunctionsProperParameters(
eventsFunctionsExtension,
eventsBasedBehavior
);
// Add the object parameter used in the behavior's events. Use
// "objectListOrEmptyIfJustDeclared" (the IDE's "Created objects"
// parameter type) so the parameter starts empty in the caller scope
// and only contains the instances created inside the action.
const objectParameter = behaviorEventsFunction
.getParameters()
.insertNewParameter(
'MyObjectParam',
behaviorEventsFunction.getParameters().getParametersCount()
);
objectParameter.setType('objectListOrEmptyIfJustDeclared');
// Scene with the two objects:
// - Bullet, the object passed as parameter
// - Cannon, the object holding the behavior
const layout = project.insertNewLayout('MyScene', 0);
layout.getObjects().insertNewObject(project, 'Sprite', 'Bullet', 0);
const cannon = layout
.getObjects()
.insertNewObject(project, 'Sprite', 'Cannon', 1);
// Register the events-based behavior on the platform so that the
// layout code generator can find the metadata for the action call.
const platformExtension = new gd.PlatformExtension();
gd.MetadataDeclarationHelper.declareExtension(
platformExtension,
eventsFunctionsExtension
);
const behaviorMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateBehaviorMetadata(
project,
platformExtension,
eventsFunctionsExtension,
eventsBasedBehavior,
behaviorMethodMangledNames
);
behaviorMethodMangledNames.delete();
gd.JsPlatform.get().addNewExtension(platformExtension);
platformExtension.delete();
cannon.addNewBehavior(
project,
'MyExtension::MyBehavior',
'MyBehavior'
);
// Scene events:
// - First event: create one Bullet directly.
// - Second event: call the behavior's MyAction passing Bullet, then
// increment Counter on the picked Bullets.
const layoutEventsSerializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'Create' },
parameters: ['', 'Bullet', '0', '0', ''],
},
],
},
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'MyExtension::MyBehavior::MyAction' },
parameters: ['Cannon', 'MyBehavior', 'Bullet'],
},
{
type: { value: 'ModVarObjet' },
parameters: ['Bullet', 'Counter', '+', '1'],
},
],
},
]);
layout
.getEvents()
.unserializeFrom(project, layoutEventsSerializerElement);
layoutEventsSerializerElement.delete();
// Serialize before we delete the project, since the mock needs the
// project/scene data to instantiate the runtime scene.
const serializedProjectElement = new gd.SerializerElement();
project.serializeTo(serializedProjectElement);
const serializedSceneElement = new gd.SerializerElement();
layout.serializeTo(serializedSceneElement);
const { gdjs, runtimeScene } = makeMinimalGDJSMock({
gameData: JSON.parse(gd.Serializer.toJSON(serializedProjectElement)),
sceneData: JSON.parse(gd.Serializer.toJSON(serializedSceneElement)),
});
// Compile the behavior - this also registers it on the gdjs mock.
const CompiledRuntimeBehavior =
generateCompiledEventsForEventsBasedBehavior(
gd,
project,
eventsFunctionsExtension,
eventsBasedBehavior,
gdjs,
{ logCode: false }
);
// Compile the layout events.
const runCompiledLayoutEvents = generateCompiledEventsForLayout(
gd,
project,
layout,
false
);
serializedProjectElement.delete();
serializedSceneElement.delete();
project.delete();
// Manually create a Cannon instance with MyBehavior attached: the
// minimal GDJS mock does not auto-instantiate behaviors from the
// scene data.
const cannonInstance = runtimeScene.createObject('Cannon');
const behaviorInstance = new CompiledRuntimeBehavior(
runtimeScene,
{ name: 'MyBehavior', type: 'MyExtension::MyBehavior' },
cannonInstance
);
cannonInstance.addBehavior(behaviorInstance);
runCompiledLayoutEvents(gdjs, runtimeScene);
// Three Bullets should exist on the scene: one created directly in
// the first scene event, plus the two created by the behavior's
// action via the object parameter.
const bulletInstances = runtimeScene.getObjects('Bullet');
expect(bulletInstances.length).toBe(3);
// Only the two Bullets created through the behavior's action must
// be picked by the increment that follows the action call. The
// Bullet created directly in the previous scene event must keep
// its Counter at 0.
const counterValues = bulletInstances
.map((instance) =>
instance.getVariables().get('Counter').getAsNumber()
)
.sort();
expect(counterValues).toEqual([0, 1, 1]);
});
});
});