Skip to content

Commit 0c1b5d1

Browse files
authored
Merge branch 'develop' into feature/hash-url-sb3-loader
2 parents a3a89bc + 86e1e0f commit 0c1b5d1

File tree

3 files changed

+258
-0
lines changed

3 files changed

+258
-0
lines changed

src/lib/ruby-to-blocks-converter/looks.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* global Opal */
22
import _ from 'lodash';
3+
import {RubyToBlocksConverterError} from './errors';
34

45
/* eslint-disable no-invalid-this */
56
const createBlockWithMessage = function (opcode, message, defaultMessage) {
@@ -27,6 +28,50 @@ const ForwardBackward = [
2728
'forward',
2829
'backward'
2930
];
31+
32+
/* eslint-disable no-invalid-this */
33+
const validateCostume = function (costumeName, args) {
34+
// Skip validation if no target context (e.g., in tests)
35+
if (!this._context.target || !this._context.target.getCostumes) {
36+
return;
37+
}
38+
39+
const costumes = this._context.target.getCostumes();
40+
const costumeExists = costumes.some(costume => costume.name === costumeName);
41+
if (!costumeExists) {
42+
throw new RubyToBlocksConverterError(
43+
args[0].node,
44+
`costume "${costumeName}" does not exist`
45+
);
46+
}
47+
};
48+
49+
const validateBackdrop = function (backdropName, args) {
50+
// Allow special backdrop values
51+
const specialBackdrops = ['next backdrop', 'previous backdrop', 'random backdrop'];
52+
if (specialBackdrops.includes(backdropName)) {
53+
return;
54+
}
55+
56+
// Skip validation if no VM context (e.g., in tests)
57+
if (!this.vm || !this.vm.runtime || !this.vm.runtime.getTargetForStage) {
58+
return;
59+
}
60+
61+
const stage = this.vm.runtime.getTargetForStage();
62+
if (!stage || !stage.getCostumes) {
63+
return;
64+
}
65+
66+
const backdrops = stage.getCostumes();
67+
const backdropExists = backdrops.some(backdrop => backdrop.name === backdropName);
68+
if (!backdropExists) {
69+
throw new RubyToBlocksConverterError(
70+
args[0].node,
71+
`backdrop "${backdropName}" does not exist`
72+
);
73+
}
74+
};
3075
/* eslint-enable no-invalid-this */
3176

3277
/**
@@ -62,18 +107,21 @@ const LooksConverter = {
62107
break;
63108
case 'switch_costume':
64109
if (args.length === 1 && this._isString(args[0])) {
110+
validateCostume.call(this, args[0].toString(), args);
65111
block = this._createBlock('looks_switchcostumeto', 'statement');
66112
this._addInput(block, 'COSTUME', this._createFieldBlock('looks_costume', 'COSTUME', args[0]));
67113
}
68114
break;
69115
case 'switch_backdrop':
70116
if (args.length === 1 && this._isString(args[0])) {
117+
validateBackdrop.call(this, args[0].toString(), args);
71118
block = this._createBlock('looks_switchbackdropto', 'statement');
72119
this._addInput(block, 'BACKDROP', this._createFieldBlock('looks_backdrops', 'BACKDROP', args[0]));
73120
}
74121
break;
75122
case 'switch_backdrop_and_wait':
76123
if (args.length === 1 && this._isString(args[0])) {
124+
validateBackdrop.call(this, args[0].toString(), args);
77125
block = this._createBlock('looks_switchbackdroptoandwait', 'statement');
78126
this._addInput(block, 'BACKDROP', this._createFieldBlock('looks_backdrops', 'BACKDROP', args[0]));
79127
}

test/helpers/ruby-helper.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import bindAll from 'lodash.bindall';
2+
import webdriver from 'selenium-webdriver';
23
import {EDIT_MENU_XPATH} from './menu-xpaths';
34

5+
const {By, until} = webdriver;
6+
47
class RubyHelper {
58
constructor (seleniumHelper) {
69
bindAll(this, [
710
'fillInRubyProgram',
811
'currentRubyProgram',
12+
'dismissAlertsIfPresent',
913
'expectInterconvertBetweenCodeAndRuby'
1014
]);
1115

@@ -27,10 +31,31 @@ class RubyHelper {
2731
return this.driver.executeScript(`ace.edit('ruby-editor').setValue('${code}');`);
2832
}
2933

34+
async dismissAlertsIfPresent () {
35+
try {
36+
// Find and dismiss any alert messages that have close buttons
37+
const alertCloseButtons = await this.driver.findElements(
38+
By.xpath('//div[contains(@class, "alert_alert-close-button")]')
39+
);
40+
for (const button of alertCloseButtons) {
41+
if (await button.isDisplayed()) {
42+
await button.click();
43+
await this.driver.sleep(100); // Wait for alert to be dismissed
44+
}
45+
}
46+
} catch (error) {
47+
// Ignore errors - alerts may not be present
48+
}
49+
}
50+
3051
async expectInterconvertBetweenCodeAndRuby (inputCode, expectedCode = null) {
3152
await this.clickText('Ruby', '*[@role="tab"]');
3253
await this.fillInRubyProgram(inputCode);
3354
await this.clickText('Code', '*[@role="tab"]');
55+
56+
// Dismiss any alerts that might be blocking subsequent clicks
57+
await this.dismissAlertsIfPresent();
58+
3459
await this.clickXpath(EDIT_MENU_XPATH);
3560
await this.clickText('Generate Ruby from Code');
3661
await this.clickText('Ruby', '*[@role="tab"]');

test/unit/lib/ruby-to-blocks-converter/looks.test.js

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,4 +899,189 @@ describe('RubyToBlocksConverter/Looks', () => {
899899
});
900900
});
901901
});
902+
903+
describe('costume existence check', () => {
904+
let targetWithCostumes;
905+
906+
beforeEach(() => {
907+
// Mock target with costumes
908+
targetWithCostumes = {
909+
getCostumes: () => [
910+
{ name: 'costume1' },
911+
{ name: 'costume2' }
912+
]
913+
};
914+
});
915+
916+
describe('switch_costume with costume existence check', () => {
917+
test('existing costume should work', () => {
918+
code = 'switch_costume("costume1")';
919+
expected = [
920+
{
921+
opcode: 'looks_switchcostumeto',
922+
inputs: [
923+
{
924+
name: 'COSTUME',
925+
block: {
926+
opcode: 'looks_costume',
927+
fields: [
928+
{
929+
name: 'COSTUME',
930+
value: 'costume1'
931+
}
932+
],
933+
shadow: true
934+
}
935+
}
936+
]
937+
}
938+
];
939+
convertAndExpectToEqualBlocks(converter, targetWithCostumes, code, expected);
940+
});
941+
942+
test('non-existing costume should throw error', () => {
943+
code = 'switch_costume("NonExistentCostume")';
944+
const result = converter.targetCodeToBlocks(targetWithCostumes, code);
945+
expect(result).toBeFalsy();
946+
expect(converter.errors).toHaveLength(1);
947+
expect(converter.errors[0].text).toContain('costume "NonExistentCostume" does not exist');
948+
});
949+
});
950+
951+
describe('costume_number and costume_name with costume existence check', () => {
952+
test('costume_number should work without costume check', () => {
953+
code = 'costume_number';
954+
expected = [
955+
{
956+
opcode: 'looks_costumenumbername',
957+
fields: [
958+
{
959+
name: 'NUMBER_NAME',
960+
value: 'number'
961+
}
962+
]
963+
}
964+
];
965+
convertAndExpectToEqualBlocks(converter, targetWithCostumes, code, expected);
966+
});
967+
968+
test('costume_name should work without costume check', () => {
969+
code = 'costume_name';
970+
expected = [
971+
{
972+
opcode: 'looks_costumenumbername',
973+
fields: [
974+
{
975+
name: 'NUMBER_NAME',
976+
value: 'name'
977+
}
978+
]
979+
}
980+
];
981+
convertAndExpectToEqualBlocks(converter, targetWithCostumes, code, expected);
982+
});
983+
});
984+
});
985+
986+
describe('backdrop existence check', () => {
987+
let stageWithBackdrops;
988+
let converterWithVM;
989+
990+
beforeEach(() => {
991+
// Mock stage with backdrops
992+
stageWithBackdrops = {
993+
getCostumes: () => [
994+
{ name: 'backdrop1' },
995+
{ name: 'backdrop2' }
996+
]
997+
};
998+
999+
// Mock converter with VM runtime
1000+
converterWithVM = new RubyToBlocksConverter({
1001+
runtime: {
1002+
getTargetForStage: () => stageWithBackdrops
1003+
}
1004+
});
1005+
});
1006+
1007+
[
1008+
{
1009+
opcode: 'looks_switchbackdropto',
1010+
methodName: 'switch_backdrop'
1011+
},
1012+
{
1013+
opcode: 'looks_switchbackdroptoandwait',
1014+
methodName: 'switch_backdrop_and_wait'
1015+
}
1016+
].forEach(info => {
1017+
describe(`${info.opcode} with backdrop existence check`, () => {
1018+
test('existing backdrop should work', () => {
1019+
code = `${info.methodName}("backdrop1")`;
1020+
expected = [
1021+
{
1022+
opcode: info.opcode,
1023+
inputs: [
1024+
{
1025+
name: 'BACKDROP',
1026+
block: {
1027+
opcode: 'looks_backdrops',
1028+
fields: [
1029+
{
1030+
name: 'BACKDROP',
1031+
value: 'backdrop1'
1032+
}
1033+
],
1034+
shadow: true
1035+
}
1036+
}
1037+
]
1038+
}
1039+
];
1040+
convertAndExpectToEqualBlocks(converterWithVM, stageWithBackdrops, code, expected);
1041+
});
1042+
1043+
test('non-existing backdrop should throw error', () => {
1044+
code = `${info.methodName}("NonExistentBackdrop")`;
1045+
const result = converterWithVM.targetCodeToBlocks(stageWithBackdrops, code);
1046+
expect(result).toBeFalsy();
1047+
expect(converterWithVM.errors).toHaveLength(1);
1048+
expect(converterWithVM.errors[0].text).toContain('backdrop "NonExistentBackdrop" does not exist');
1049+
});
1050+
});
1051+
});
1052+
1053+
describe('backdrop_number and backdrop_name with backdrop existence check', () => {
1054+
test('backdrop_number should work without backdrop check', () => {
1055+
code = 'backdrop_number';
1056+
expected = [
1057+
{
1058+
opcode: 'looks_backdropnumbername',
1059+
fields: [
1060+
{
1061+
name: 'NUMBER_NAME',
1062+
value: 'number'
1063+
}
1064+
]
1065+
}
1066+
];
1067+
convertAndExpectToEqualBlocks(converterWithVM, stageWithBackdrops, code, expected);
1068+
});
1069+
1070+
test('backdrop_name should work without backdrop check', () => {
1071+
code = 'backdrop_name';
1072+
expected = [
1073+
{
1074+
opcode: 'looks_backdropnumbername',
1075+
fields: [
1076+
{
1077+
name: 'NUMBER_NAME',
1078+
value: 'name'
1079+
}
1080+
]
1081+
}
1082+
];
1083+
convertAndExpectToEqualBlocks(converterWithVM, stageWithBackdrops, code, expected);
1084+
});
1085+
});
1086+
});
9021087
});

0 commit comments

Comments
 (0)