Files
GDevelop/GDevelop.js/__tests__/GDJSDisabledCodeGenerationIntegrationTests.js
Florian Rival abe44723ba Fix else events not working properly when after a disabled event or comment (#8293)
- Also fix generated code for events (and Else) not working with 'use strict' (only show in developer changelog)
- Also improve the UI to not show horizontal bars for disabled events and comments
2026-02-19 17:14:24 +01:00

669 lines
21 KiB
JavaScript

const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js');
const { makeMinimalGDJSMock } = require('../TestUtils/GDJSMocks.js');
const {
generateCompiledEventsFromSerializedEvents,
} = require('../TestUtils/CodeGenerationHelpers.js');
describe('libGD.js - GDJS Transparent Events Code Generation integration tests', function () {
let gd = null;
beforeAll(async () => {
gd = await initializeGDevelopJs();
});
// ── Helpers to build events concisely ──────────────────────────────────
// All conditions compare scene variable "X" which is never modified
// (defaults to 0), so "X == 0" is always true, "X == 999" is always false.
const trueCondition = [
{ type: { value: 'VarScene' }, parameters: ['X', '=', '0'] },
];
const falseCondition = [
{ type: { value: 'VarScene' }, parameters: ['X', '=', '999'] },
];
const setVar = (name, value) => ({
type: { value: 'ModVarScene' },
parameters: [name, '=', String(value)],
});
const addVar = (name, value) => ({
type: { value: 'ModVarScene' },
parameters: [name, '+', String(value)],
});
const std = (varName, value, { conditions, subEvents } = {}) => ({
type: 'BuiltinCommonInstructions::Standard',
conditions: conditions || [],
actions: [setVar(varName, value)],
events: subEvents || [],
});
const stdFalse = (varName, value, opts = {}) =>
std(varName, value, { ...opts, conditions: falseCondition });
const stdTrue = (varName, value, opts = {}) =>
std(varName, value, { ...opts, conditions: trueCondition });
const elseEv = (varName, value, { conditions, subEvents } = {}) => ({
type: 'BuiltinCommonInstructions::Else',
conditions: conditions || [],
actions: [setVar(varName, value)],
events: subEvents || [],
});
const elseIfTrue = (varName, value, opts = {}) =>
elseEv(varName, value, { ...opts, conditions: trueCondition });
const elseIfFalse = (varName, value, opts = {}) =>
elseEv(varName, value, { ...opts, conditions: falseCondition });
const repeat = (times, subEvents) => ({
type: 'BuiltinCommonInstructions::Repeat',
repeatExpression: String(times),
conditions: [],
actions: [],
events: subEvents || [],
});
const whileLoop = (limit, subEvents) => ({
type: 'BuiltinCommonInstructions::While',
whileConditions: [
{
type: { value: 'VarScene' },
parameters: ['Counter', '<', String(limit)],
},
],
conditions: [],
actions: [addVar('Counter', 1)],
events: subEvents || [],
});
const chainBreaker = () => repeat(1, []);
const disabled = (event) => ({ ...event, disabled: true });
const comment = () => ({ type: 'BuiltinCommonInstructions::Comment' });
function runEvents(events, options = {}) {
const serializerElement = gd.Serializer.fromJSObject(events);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement,
options
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runCompiledEvents(gdjs, runtimeScene, []);
return (name) =>
runtimeScene.getVariables().has(name)
? runtimeScene.getVariables().get(name).getAsNumber()
: 0;
}
// ── Modes: both disabled events and comment events must be transparent ─
const transparentEventModes = [
{
label: 'disabled events',
// Wraps any event to make it transparent (disabled).
wrap: (event) => disabled(event),
// Various transparent events to insert in stress tests.
variants: () => [
disabled(std('_noop_', 99)),
disabled(stdFalse('_noop_', 99)),
disabled(stdTrue('_noop_', 99)),
disabled(elseEv('_noop_', 99)),
disabled(elseIfTrue('_noop_', 99)),
disabled(elseIfFalse('_noop_', 99)),
],
// A cluster of transparent events for multi-insert stress test.
cluster: () => [
disabled(std('_n1_', 99)),
disabled(elseEv('_n2_', 99)),
disabled(stdTrue('_n3_', 99)),
],
},
{
label: 'comment events',
wrap: () => comment(),
variants: () => [comment()],
cluster: () => [comment(), comment(), comment()],
},
];
transparentEventModes.forEach(({ label, wrap, variants, cluster }) => {
describe(`with ${label}`, function () {
// ── Individual tests ─────────────────────────────────────────
it('transparent event between Standard(true) and Else does not affect chain', function () {
const v = runEvents([
stdTrue('If', 1),
wrap(std('T', 99)),
elseEv('Else', 1),
]);
expect(v('If')).toBe(1);
expect(v('T')).toBe(0);
expect(v('Else')).toBe(0);
});
it('transparent event between Standard(false) and Else does not affect chain', function () {
const v = runEvents([
stdFalse('If', 1),
wrap(std('T', 99)),
elseEv('Else', 1),
]);
expect(v('If')).toBe(0);
expect(v('T')).toBe(0);
expect(v('Else')).toBe(1);
});
it('multiple transparent events between Standard(true) and Else do not affect chain', function () {
const v = runEvents([
stdTrue('If', 1),
wrap(std('T1', 99)),
wrap(stdFalse('T2', 99)),
wrap(elseEv('T3', 99)),
elseEv('Else', 1),
]);
expect(v('If')).toBe(1);
expect(v('T1')).toBe(0);
expect(v('T2')).toBe(0);
expect(v('T3')).toBe(0);
expect(v('Else')).toBe(0);
});
it('multiple transparent events between Standard(false) and Else do not affect chain', function () {
const v = runEvents([
stdFalse('If', 1),
wrap(std('T1', 99)),
wrap(stdFalse('T2', 99)),
wrap(elseEv('T3', 99)),
elseEv('Else', 1),
]);
expect(v('If')).toBe(0);
expect(v('T1')).toBe(0);
expect(v('T2')).toBe(0);
expect(v('T3')).toBe(0);
expect(v('Else')).toBe(1);
});
it('transparent Else between Standard(false) and Else does not affect chain', function () {
const v = runEvents([
stdFalse('If', 1),
wrap(elseIfTrue('T', 99)),
elseEv('Else', 1),
]);
expect(v('If')).toBe(0);
expect(v('T')).toBe(0);
expect(v('Else')).toBe(1);
});
it('transparent Else between Standard(true) and Else does not affect chain', function () {
const v = runEvents([
stdTrue('If', 1),
wrap(elseEv('T', 99)),
elseEv('Else', 1),
]);
expect(v('If')).toBe(1);
expect(v('T')).toBe(0);
expect(v('Else')).toBe(0);
});
it('all transparent events before Else makes it standalone (runs)', function () {
const v = runEvents([
chainBreaker(),
wrap(std('T1', 99)),
wrap(stdFalse('T2', 99)),
elseEv('Else', 1),
]);
expect(v('T1')).toBe(0);
expect(v('T2')).toBe(0);
expect(v('Else')).toBe(1);
});
it('transparent event in an else-if chain preserves chain semantics', function () {
const v = runEvents([
stdFalse('If', 1),
wrap(elseIfTrue('T', 99)),
elseIfFalse('Ei', 1),
elseEv('Else', 1),
]);
expect(v('If')).toBe(0);
expect(v('T')).toBe(0);
expect(v('Ei')).toBe(0);
expect(v('Else')).toBe(1);
});
it('transparent event between two Else events in a chain is transparent', function () {
const v = runEvents([
stdFalse('If', 1),
elseIfFalse('Ei1', 1),
wrap(elseIfTrue('T', 99)),
elseIfTrue('Ei2', 1),
elseEv('Else', 1),
]);
expect(v('If')).toBe(0);
expect(v('Ei1')).toBe(0);
expect(v('T')).toBe(0);
expect(v('Ei2')).toBe(1);
expect(v('Else')).toBe(0);
});
it('transparent Standard does not break an else chain', function () {
const v = runEvents([
stdFalse('If', 1),
elseIfFalse('Ei', 1),
wrap(std('T', 99)),
elseEv('Else', 1),
]);
expect(v('If')).toBe(0);
expect(v('Ei')).toBe(0);
expect(v('T')).toBe(0);
expect(v('Else')).toBe(1);
});
it('transparent event inside Repeat does not affect if/else inside', function () {
const events = [
{
type: 'BuiltinCommonInstructions::Repeat',
repeatExpression: '3',
conditions: [],
actions: [],
events: [
{
type: 'BuiltinCommonInstructions::Standard',
conditions: falseCondition,
actions: [addVar('RepIf', 1)],
events: [],
},
wrap(std('T', 99)),
{
type: 'BuiltinCommonInstructions::Else',
conditions: [],
actions: [addVar('RepElse', 1)],
events: [],
},
],
},
];
const v = runEvents(events);
expect(v('RepIf')).toBe(0);
expect(v('T')).toBe(0);
expect(v('RepElse')).toBe(3);
});
it('transparent event inside While sub-events does not affect if/else inside', function () {
const events = [
{
type: 'BuiltinCommonInstructions::While',
whileConditions: [
{
type: { value: 'VarScene' },
parameters: ['Counter', '<', '3'],
},
],
conditions: [],
actions: [addVar('Counter', 1)],
events: [
{
type: 'BuiltinCommonInstructions::Standard',
conditions: falseCondition,
actions: [addVar('WIf', 1)],
events: [],
},
wrap(std('T', 99)),
{
type: 'BuiltinCommonInstructions::Else',
conditions: [],
actions: [addVar('WElse', 1)],
events: [],
},
],
},
];
const v = runEvents(events);
expect(v('Counter')).toBe(3);
expect(v('WIf')).toBe(0);
expect(v('T')).toBe(0);
expect(v('WElse')).toBe(3);
});
it('transparent events in sub-events of an Else do not affect chains', function () {
const v = runEvents([
stdFalse('If', 1),
elseEv('Else', 1, {
subEvents: [
stdFalse('SubIf', 1),
wrap(std('T', 99)),
elseEv('SubElse', 1),
],
}),
]);
expect(v('If')).toBe(0);
expect(v('Else')).toBe(1);
expect(v('SubIf')).toBe(0);
expect(v('T')).toBe(0);
expect(v('SubElse')).toBe(1);
});
it('transparent events in sub-events of a Standard do not affect chains', function () {
const v = runEvents([
std('Parent', 1, {
subEvents: [
stdTrue('SubIf', 1),
wrap(elseIfTrue('T', 99)),
elseEv('SubElse', 1),
],
}),
]);
expect(v('Parent')).toBe(1);
expect(v('SubIf')).toBe(1);
expect(v('T')).toBe(0);
expect(v('SubElse')).toBe(0);
});
// ── Stress tests ───────────────────────────────────────────
it('stress test: inserting at every position in top-level events does not change outcomes', function () {
const baseEvents = [
stdFalse('C1_if', 1),
elseIfFalse('C1_ei1', 1),
elseIfTrue('C1_ei2', 1),
elseEv('C1_else', 1),
stdTrue('C2_if', 1),
elseEv('C2_else', 1),
repeat(2, [
stdFalse('C3_repIf', 1),
elseEv('C3_repElse', 0),
]),
std('C4_std1', 1),
stdFalse('C4_std2', 1),
elseIfFalse('C4_ei', 1),
elseEv('C4_else', 1),
stdFalse('C5_if', 1),
elseEv('C5_else', 1, {
subEvents: [
stdFalse('C5_subIf', 1),
elseIfTrue('C5_subEi', 1),
elseEv('C5_subElse', 1),
],
}),
whileLoop(2, [
stdFalse('C6_wIf', 1),
elseEv('C6_wElse', 0),
]),
std('C7_after', 1),
];
const variableNames = [
'C1_if', 'C1_ei1', 'C1_ei2', 'C1_else',
'C2_if', 'C2_else',
'C3_repIf', 'C3_repElse',
'C4_std1', 'C4_std2', 'C4_ei', 'C4_else',
'C5_if', 'C5_else', 'C5_subIf', 'C5_subEi', 'C5_subElse',
'C6_wIf', 'C6_wElse',
'C7_after',
];
const vBase = runEvents(baseEvents);
const expected = {};
for (const name of variableNames) {
expected[name] = vBase(name);
}
expect(expected['C1_ei2']).toBe(1);
expect(expected['C1_else']).toBe(0);
expect(expected['C2_if']).toBe(1);
expect(expected['C2_else']).toBe(0);
expect(expected['C4_else']).toBe(1);
expect(expected['C5_else']).toBe(1);
expect(expected['C5_subEi']).toBe(1);
expect(expected['C7_after']).toBe(1);
const allVariants = variants();
for (let pos = 0; pos <= baseEvents.length; pos++) {
for (const transparentEv of allVariants) {
const modified = [...baseEvents];
modified.splice(pos, 0, transparentEv);
const v = runEvents(modified);
for (const name of variableNames) {
if (v(name) !== expected[name]) {
expect({
variable: name,
position: pos,
insertedType: transparentEv.type,
actual: v(name),
expected: expected[name],
}).toEqual({
variable: name,
position: pos,
insertedType: transparentEv.type,
actual: expected[name],
expected: expected[name],
});
}
}
}
}
});
it('stress test: inserting at every position in sub-events does not change outcomes', function () {
const baseSubEvents = [
stdFalse('S_if', 1),
elseIfFalse('S_ei1', 1),
elseIfTrue('S_ei2', 1, {
subEvents: [
stdFalse('S_nested_if', 1),
elseEv('S_nested_else', 1),
],
}),
elseEv('S_else', 1),
std('S_after', 1),
];
const baseEvents = [
std('Parent', 1, { subEvents: baseSubEvents }),
];
const variableNames = [
'Parent', 'S_if', 'S_ei1', 'S_ei2', 'S_nested_if',
'S_nested_else', 'S_else', 'S_after',
];
const vBase = runEvents(baseEvents);
const expected = {};
for (const name of variableNames) {
expected[name] = vBase(name);
}
expect(expected['Parent']).toBe(1);
expect(expected['S_ei2']).toBe(1);
expect(expected['S_nested_else']).toBe(1);
expect(expected['S_else']).toBe(0);
expect(expected['S_after']).toBe(1);
const allVariants = variants();
for (let pos = 0; pos <= baseSubEvents.length; pos++) {
for (const transparentEv of allVariants) {
const modifiedSubEvents = [...baseSubEvents];
modifiedSubEvents.splice(pos, 0, transparentEv);
const modifiedEvents = [
std('Parent', 1, { subEvents: modifiedSubEvents }),
];
const v = runEvents(modifiedEvents);
for (const name of variableNames) {
if (v(name) !== expected[name]) {
expect({
variable: name,
position: pos,
insertedType: transparentEv.type,
actual: v(name),
expected: expected[name],
}).toEqual({
variable: name,
position: pos,
insertedType: transparentEv.type,
actual: expected[name],
expected: expected[name],
});
}
}
}
}
});
it('stress test: inserting at every position inside Repeat does not change outcomes', function () {
const baseRepeatBody = [
stdFalse('R_if', 1),
elseIfFalse('R_ei', 1),
elseEv('R_else', 0),
std('R_after', 0),
];
const baseEvents = [repeat(3, baseRepeatBody)];
const variableNames = ['R_if', 'R_ei', 'R_else', 'R_after'];
const vBase = runEvents(baseEvents);
const expected = {};
for (const name of variableNames) {
expected[name] = vBase(name);
}
const allVariants = variants();
for (let pos = 0; pos <= baseRepeatBody.length; pos++) {
for (const transparentEv of allVariants) {
const modifiedBody = [...baseRepeatBody];
modifiedBody.splice(pos, 0, transparentEv);
const modifiedEvents = [repeat(3, modifiedBody)];
const v = runEvents(modifiedEvents);
for (const name of variableNames) {
if (v(name) !== expected[name]) {
expect({
variable: name,
position: pos,
insertedType: transparentEv.type,
actual: v(name),
expected: expected[name],
}).toEqual({
variable: name,
position: pos,
insertedType: transparentEv.type,
actual: expected[name],
expected: expected[name],
});
}
}
}
}
});
it('stress test: inserting at every position inside While does not change outcomes', function () {
const baseWhileBody = [
stdFalse('W_if', 1),
elseIfTrue('W_ei', 0),
elseEv('W_else', 1),
];
const baseEvents = [whileLoop(3, baseWhileBody)];
const variableNames = ['Counter', 'W_if', 'W_ei', 'W_else'];
const vBase = runEvents(baseEvents);
const expected = {};
for (const name of variableNames) {
expected[name] = vBase(name);
}
expect(expected['Counter']).toBe(3);
expect(expected['W_ei']).toBe(0);
expect(expected['W_else']).toBe(0);
const allVariants = variants();
for (let pos = 0; pos <= baseWhileBody.length; pos++) {
for (const transparentEv of allVariants) {
const modifiedBody = [...baseWhileBody];
modifiedBody.splice(pos, 0, transparentEv);
const modifiedEvents = [whileLoop(3, modifiedBody)];
const v = runEvents(modifiedEvents);
for (const name of variableNames) {
if (v(name) !== expected[name]) {
expect({
variable: name,
position: pos,
insertedType: transparentEv.type,
actual: v(name),
expected: expected[name],
}).toEqual({
variable: name,
position: pos,
insertedType: transparentEv.type,
actual: expected[name],
expected: expected[name],
});
}
}
}
}
});
it('stress test: multiple transparent events inserted at every position simultaneously', function () {
const baseEvents = [
stdFalse('M_if', 1),
elseIfFalse('M_ei1', 1),
elseIfTrue('M_ei2', 1),
elseEv('M_else', 1),
std('M_after', 1),
];
const variableNames = [
'M_if', 'M_ei1', 'M_ei2', 'M_else', 'M_after',
];
const vBase = runEvents(baseEvents);
const expected = {};
for (const name of variableNames) {
expected[name] = vBase(name);
}
expect(expected['M_ei2']).toBe(1);
expect(expected['M_else']).toBe(0);
expect(expected['M_after']).toBe(1);
const clusterEvents = cluster();
for (let pos = 0; pos <= baseEvents.length; pos++) {
const modified = [...baseEvents];
modified.splice(pos, 0, ...clusterEvents);
const v = runEvents(modified);
for (const name of variableNames) {
if (v(name) !== expected[name]) {
expect({
variable: name,
position: pos,
actual: v(name),
expected: expected[name],
}).toEqual({
variable: name,
position: pos,
actual: expected[name],
expected: expected[name],
});
}
}
}
});
});
});
});