File "workspace_test.js"
Full path: /usr/home/mndrn/domains/mndrn.ru/public_html/block-hill/blockly/tests/mocha/workspace_test.js
File size: 55.77 KiB (57109 bytes)
MIME-type: text/plain
Charset: utf-8
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
suite('Workspace', function() {
setup(function() {
sharedTestSetup.call(this);
this.workspace = new Blockly.Workspace();
});
teardown(function() {
sharedTestTeardown.call(this);
});
// eslint-disable-next-line no-use-before-define
testAWorkspace();
});
function testAWorkspace() {
setup(function() {
Blockly.defineBlocksWithJsonArray([{
"type": "get_var_block",
"message0": "%1",
"args0": [
{
"type": "field_variable",
"name": "VAR",
"variableTypes": ["", "type1", "type2"]
}
]
}]);
});
teardown(function() {
// Clear Blockly.Event state.
Blockly.Events.setGroup(false);
Blockly.Events.disabled_ = 0;
sinon.restore();
});
function assertBlockVarModelName(workspace, blockIndex, name) {
var block = workspace.topBlocks_[blockIndex];
chai.assert.exists(block, 'Block at topBlocks_[' + blockIndex + ']');
var varModel = block.getVarModels()[0];
chai.assert.exists(varModel,
'VariableModel for block at topBlocks_[' + blockIndex + ']');
var blockVarName = varModel.name;
chai.assert.equal(blockVarName, name,
'VariableModel name for block at topBlocks_[' + blockIndex + ']');
}
function createVarBlocksNoEvents(workspace, ids) {
var blocks = [];
// Turn off events to avoid testing XML at the same time.
Blockly.Events.disable();
for (var i = 0, id; (id = ids[i]); i++) {
var block = new Blockly.Block(workspace, 'get_var_block');
block.inputList[0].fieldRow[0].setValue(id);
blocks.push(block);
}
Blockly.Events.enable();
return blocks;
}
suite('clear', function() {
test('Trivial', function() {
sinon.stub(Blockly.Events, "setGroup").returns(null);
this.workspace.createVariable('name1', 'type1', 'id1');
this.workspace.createVariable('name2', 'type2', 'id2');
this.workspace.newBlock('');
this.workspace.clear();
chai.assert.equal(this.workspace.topBlocks_.length, 0);
var varMapLength =
Object.keys(this.workspace.variableMap_.variableMap_).length;
chai.assert.equal(varMapLength, 0);
});
test('No variables', function() {
sinon.stub(Blockly.Events, "setGroup").returns(null);
this.workspace.newBlock('');
this.workspace.clear();
chai.assert.equal(this.workspace.topBlocks_.length, 0);
var varMapLength =
Object.keys(this.workspace.variableMap_.variableMap_).length;
chai.assert.equal(varMapLength, 0);
});
});
suite('deleteVariable', function() {
setup(function() {
// Create two variables of different types.
this.var1 = this.workspace.createVariable('name1', 'type1', 'id1');
this.var2 = this.workspace.createVariable('name2', 'type2', 'id2');
// Create blocks to refer to both of them.
createVarBlocksNoEvents(this.workspace, ['id1', 'id1', 'id2']);
});
test('deleteVariableById(id2) one usage', function() {
// Deleting variable one usage should not trigger confirm dialog.
var stub =
sinon.stub(Blockly, "confirm").callsArgWith(1, true);
this.workspace.deleteVariableById('id2');
sinon.assert.notCalled(stub);
var variable = this.workspace.getVariableById('id2');
chai.assert.isNull(variable);
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertBlockVarModelName(this.workspace, 0, 'name1');
});
test('deleteVariableById(id1) multiple usages confirm', function() {
// Deleting variable with multiple usages triggers confirm dialog.
var stub =
sinon.stub(Blockly, "confirm").callsArgWith(1, true);
this.workspace.deleteVariableById('id1');
sinon.assert.calledOnce(stub);
var variable = this.workspace.getVariableById('id1');
chai.assert.isNull(variable);
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
assertBlockVarModelName(this.workspace, 0, 'name2');
});
test('deleteVariableById(id1) multiple usages cancel', function() {
// Deleting variable with multiple usages triggers confirm dialog.
var stub =
sinon.stub(Blockly, "confirm").callsArgWith(1, false);
this.workspace.deleteVariableById('id1');
sinon.assert.calledOnce(stub);
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
assertBlockVarModelName(this.workspace, 0, 'name1');
assertBlockVarModelName(this.workspace, 1, 'name1');
assertBlockVarModelName(this.workspace, 2, 'name2');
});
});
suite('renameVariableById', function() {
setup(function() {
this.workspace.createVariable('name1', 'type1', 'id1');
});
test('No references rename to name2', function() {
this.workspace.renameVariableById('id1', 'name2');
assertVariableValues(this.workspace, 'name2', 'type1', 'id1');
// Renaming should not have created a new variable.
chai.assert.equal(this.workspace.getAllVariables().length, 1);
});
test('Reference exists rename to name2', function() {
createVarBlocksNoEvents(this.workspace, ['id1']);
this.workspace.renameVariableById('id1', 'name2');
assertVariableValues(this.workspace, 'name2', 'type1', 'id1');
// Renaming should not have created a new variable.
chai.assert.equal(this.workspace.getAllVariables().length, 1);
assertBlockVarModelName(this.workspace, 0, 'name2');
});
test('Reference exists different capitalization rename to Name1', function() {
createVarBlocksNoEvents(this.workspace, ['id1']);
this.workspace.renameVariableById('id1', 'Name1');
assertVariableValues(this.workspace, 'Name1', 'type1', 'id1');
// Renaming should not have created a new variable.
chai.assert.equal(this.workspace.getAllVariables().length, 1);
assertBlockVarModelName(this.workspace, 0, 'Name1');
});
suite('Two variables rename overlap', function() {
test('Same type rename variable with id1 to name2', function() {
this.workspace.createVariable('name2', 'type1', 'id2');
createVarBlocksNoEvents(this.workspace, ['id1', 'id2']);
this.workspace.renameVariableById('id1', 'name2');
// The second variable should remain unchanged.
assertVariableValues(this.workspace, 'name2', 'type1', 'id2');
// The first variable should have been deleted.
var variable = this.workspace.getVariableById('id1');
chai.assert.isNull(variable);
// There should only be one variable left.
chai.assert.equal(this.workspace.getAllVariables().length, 1);
// Both blocks should now reference variable with name2.
assertBlockVarModelName(this.workspace, 0, 'name2');
assertBlockVarModelName(this.workspace, 1, 'name2');
});
test('Different type rename variable with id1 to name2', function() {
this.workspace.createVariable('name2', 'type2', 'id2');
createVarBlocksNoEvents(this.workspace, ['id1', 'id2']);
this.workspace.renameVariableById('id1', 'name2');
// Variables with different type are allowed to have the same name.
assertVariableValues(this.workspace, 'name2', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
// Both blocks should now reference variable with name2.
assertBlockVarModelName(this.workspace, 0, 'name2');
assertBlockVarModelName(this.workspace, 1, 'name2');
});
test('Same type different capitalization rename variable with id1 to Name2', function() {
this.workspace.createVariable('name2', 'type1', 'id2');
createVarBlocksNoEvents(this.workspace, ['id1', 'id2']);
this.workspace.renameVariableById('id1', 'Name2');
// The second variable should be updated.
assertVariableValues(this.workspace, 'Name2', 'type1', 'id2');
// The first variable should have been deleted.
var variable = this.workspace.getVariableById('id1');
chai.assert.isNull(variable);
// There should only be one variable left.
chai.assert.equal(this.workspace.getAllVariables().length, 1);
// Both blocks should now reference variable with Name2.
assertBlockVarModelName(this.workspace, 0, 'Name2');
assertBlockVarModelName(this.workspace, 1, 'Name2');
});
test('Different type different capitalization rename variable with id1 to Name2', function() {
this.workspace.createVariable('name2', 'type2', 'id2');
createVarBlocksNoEvents(this.workspace, ['id1', 'id2']);
this.workspace.renameVariableById('id1', 'Name2');
// Variables with different type are allowed to have the same name.
assertVariableValues(this.workspace, 'Name2', 'type1', 'id1');
// Second variable should remain unchanged.
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
// Only first block should use new capitalization.
assertBlockVarModelName(this.workspace, 0, 'Name2');
assertBlockVarModelName(this.workspace, 1, 'name2');
});
});
});
suite('getTopBlocks(ordered=true)', function() {
test('Empty workspace', function() {
chai.assert.equal(this.workspace.getTopBlocks(true).length, 0);
});
test('Flat workspace one block', function() {
this.workspace.newBlock('');
chai.assert.equal(this.workspace.getTopBlocks(true).length, 1);
});
test('Flat workspace one block after dispose', function() {
var blockA = this.workspace.newBlock('');
this.workspace.newBlock('');
blockA.dispose();
chai.assert.equal(this.workspace.getTopBlocks(true).length, 1);
});
test('Flat workspace two blocks', function() {
this.workspace.newBlock('');
this.workspace.newBlock('');
chai.assert.equal(this.workspace.getTopBlocks(true).length, 2);
});
test('Clear', function() {
this.workspace.clear();
chai.assert.equal(this.workspace.getTopBlocks(true).length, 0,
'Clear empty workspace');
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.clear();
chai.assert.equal(this.workspace.getTopBlocks(true).length, 0);
});
});
suite('getTopBlocks(ordered=false)', function() {
test('Empty workspace', function() {
chai.assert.equal(this.workspace.getTopBlocks(false).length, 0);
});
test('Flat workspace one block', function() {
this.workspace.newBlock('');
chai.assert.equal(this.workspace.getTopBlocks(false).length, 1);
});
test('Flat workspace one block after dispose', function() {
var blockA = this.workspace.newBlock('');
this.workspace.newBlock('');
blockA.dispose();
chai.assert.equal(this.workspace.getTopBlocks(false).length, 1);
});
test('Flat workspace two blocks', function() {
this.workspace.newBlock('');
this.workspace.newBlock('');
chai.assert.equal(this.workspace.getTopBlocks(false).length, 2);
});
test('Clear empty workspace', function() {
this.workspace.clear();
chai.assert.equal(this.workspace.getTopBlocks(false).length, 0);
});
test('Clear non-empty workspace', function() {
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.clear();
chai.assert.equal(this.workspace.getTopBlocks(false).length, 0);
});
});
suite('getAllBlocks', function() {
test('Empty workspace', function() {
chai.assert.equal(this.workspace.getAllBlocks(true).length, 0);
});
test('Flat workspace one block', function() {
this.workspace.newBlock('');
chai.assert.equal(this.workspace.getAllBlocks(true).length, 1);
});
test('Flat workspace one block after dispose', function() {
var blockA = this.workspace.newBlock('');
this.workspace.newBlock('');
blockA.dispose();
chai.assert.equal(this.workspace.getAllBlocks(true).length, 1);
});
test('Flat workspace two blocks', function() {
this.workspace.newBlock('');
this.workspace.newBlock('');
chai.assert.equal(this.workspace.getAllBlocks(true).length, 2);
});
test('Clear', function() {
this.workspace.clear();
chai.assert.equal(this.workspace.getAllBlocks(true).length, 0,
'Clear empty workspace');
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.clear();
chai.assert.equal(this.workspace.getAllBlocks(true).length, 0);
});
});
suite('remainingCapacity', function() {
setup(function() {
this.workspace.newBlock('');
this.workspace.newBlock('');
});
test('No block limit', function() {
chai.assert.equal(this.workspace.remainingCapacity(), Infinity);
});
test('Under block limit', function() {
this.workspace.options.maxBlocks = 3;
chai.assert.equal(this.workspace.remainingCapacity(), 1);
this.workspace.options.maxBlocks = 4;
chai.assert.equal(this.workspace.remainingCapacity(), 2);
});
test('At block limit', function() {
this.workspace.options.maxBlocks = 2;
chai.assert.equal(this.workspace.remainingCapacity(), 0);
});
test('At block limit of 0 after clear', function() {
this.workspace.options.maxBlocks = 0;
this.workspace.clear();
chai.assert.equal(this.workspace.remainingCapacity(), 0);
});
test('Over block limit', function() {
this.workspace.options.maxBlocks = 1;
chai.assert.equal(this.workspace.remainingCapacity(), -1);
});
test('Over block limit of 0', function() {
this.workspace.options.maxBlocks = 0;
chai.assert.equal(this.workspace.remainingCapacity(), -2);
});
});
suite('remainingCapacityOfType', function() {
setup(function() {
this.workspace.newBlock('get_var_block');
this.workspace.newBlock('get_var_block');
this.workspace.options.maxInstances = {};
});
test('No instance limit', function() {
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
Infinity);
});
test('Under instance limit', function() {
this.workspace.options.maxInstances['get_var_block'] = 3;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
1, 'With maxInstances limit 3');
this.workspace.options.maxInstances['get_var_block'] = 4;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
2, 'With maxInstances limit 4');
});
test('Under instance limit with multiple block types', function() {
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.options.maxInstances['get_var_block'] = 3;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
1, 'With maxInstances limit 3');
this.workspace.options.maxInstances['get_var_block'] = 4;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
2, 'With maxInstances limit 4');
});
test('At instance limit', function() {
this.workspace.options.maxInstances['get_var_block'] = 2;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
0, 'With maxInstances limit 2');
});
test('At instance limit of 0 after clear', function() {
this.workspace.clear();
this.workspace.options.maxInstances['get_var_block'] = 0;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
0);
});
test('At instance limit with multiple block types', function() {
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.options.maxInstances['get_var_block'] = 2;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
0, 'With maxInstances limit 2');
});
test('At instance limit of 0 with multiple block types', function() {
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.options.maxInstances['get_var_block'] = 0;
this.workspace.clear();
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
0);
});
test('Over instance limit', function() {
this.workspace.options.maxInstances['get_var_block'] = 1;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
-1,'With maxInstances limit 1');
});
test('Over instance limit of 0', function() {
this.workspace.options.maxInstances['get_var_block'] = 0;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
-2,'With maxInstances limit 0');
});
test('Over instance limit with multiple block types', function() {
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.options.maxInstances['get_var_block'] = 1;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
-1,'With maxInstances limit 1');
});
test('Over instance limit of 0 with multiple block types', function() {
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.newBlock('');
this.workspace.options.maxInstances['get_var_block'] = 0;
chai.assert.equal(this.workspace.remainingCapacityOfType('get_var_block'),
-2,'With maxInstances limit 0');
});
});
suite('isCapacityAvailable', function() {
setup(function() {
this.workspace.newBlock('get_var_block');
this.workspace.newBlock('get_var_block');
this.workspace.options.maxInstances = {};
});
test('Under block limit and no instance limit', function() {
this.workspace.options.maxBlocks = 3;
var typeCountsMap = {'get_var_block': 1};
chai.assert.isTrue(this.workspace.isCapacityAvailable(typeCountsMap));
});
test('At block limit and no instance limit', function() {
this.workspace.options.maxBlocks = 2;
var typeCountsMap = {'get_var_block': 1};
chai.assert.isFalse(this.workspace.isCapacityAvailable(typeCountsMap));
});
test('Over block limit of 0 and no instance limit', function() {
this.workspace.options.maxBlocks = 0;
var typeCountsMap = {'get_var_block': 1};
chai.assert.isFalse(this.workspace.isCapacityAvailable(typeCountsMap));
});
test('Over block limit but under instance limit', function() {
this.workspace.options.maxBlocks = 1;
this.workspace.options.maxInstances['get_var_block'] = 3;
var typeCountsMap = {'get_var_block': 1};
chai.assert.isFalse(this.workspace.isCapacityAvailable(typeCountsMap),
'With maxBlocks limit 1 and maxInstances limit 3');
});
test('Over block limit of 0 but under instance limit', function() {
this.workspace.options.maxBlocks = 0;
this.workspace.options.maxInstances['get_var_block'] = 3;
var typeCountsMap = {'get_var_block': 1};
chai.assert.isFalse(this.workspace.isCapacityAvailable(typeCountsMap),
'With maxBlocks limit 0 and maxInstances limit 3');
});
test('Over block limit but at instance limit', function() {
this.workspace.options.maxBlocks = 1;
this.workspace.options.maxInstances['get_var_block'] = 2;
var typeCountsMap = {'get_var_block': 1};
chai.assert.isFalse(this.workspace.isCapacityAvailable(typeCountsMap),
'With maxBlocks limit 1 and maxInstances limit 2');
});
test('Over block limit and over instance limit', function() {
this.workspace.options.maxBlocks = 1;
this.workspace.options.maxInstances['get_var_block'] = 1;
var typeCountsMap = {'get_var_block': 1};
chai.assert.isFalse(this.workspace.isCapacityAvailable(typeCountsMap),
'With maxBlocks limit 1 and maxInstances limit 1');
});
test('Over block limit of 0 and over instance limit', function() {
this.workspace.options.maxBlocks = 0;
this.workspace.options.maxInstances['get_var_block'] = 1;
var typeCountsMap = {'get_var_block': 1};
chai.assert.isFalse(this.workspace.isCapacityAvailable(typeCountsMap),
'With maxBlocks limit 0 and maxInstances limit 1');
});
test('Over block limit and over instance limit of 0', function() {
this.workspace.options.maxBlocks = 1;
this.workspace.options.maxInstances['get_var_block'] = 0;
var typeCountsMap = {'get_var_block': 1};
chai.assert.isFalse(this.workspace.isCapacityAvailable(typeCountsMap),
'With maxBlocks limit 1 and maxInstances limit 0');
});
test('Over block limit of 0 and over instance limit of 0', function() {
this.workspace.options.maxBlocks = 0;
this.workspace.options.maxInstances['get_var_block'] = 0;
var typeCountsMap = {'get_var_block': 1};
chai.assert.isFalse(this.workspace.isCapacityAvailable(typeCountsMap));
});
});
suite('getById', function() {
setup(function() {
this.workspaceB = this.workspace.rendered ?
new Blockly.WorkspaceSvg(new Blockly.Options({})) :
new Blockly.Workspace();
});
teardown(function() {
workspaceTeardown.call(this, this.workspaceB);
});
test('Trivial', function() {
chai.assert.equal(Blockly.Workspace.getById(
this.workspace.id), this.workspace, 'Find workspace');
chai.assert.equal(Blockly.Workspace.getById(
this.workspaceB.id), this.workspaceB, 'Find workspaceB');
});
test('Null id', function() {
chai.assert.isNull(Blockly.Workspace.getById(null));
});
test('Non-existent id', function() {
chai.assert.isNull(Blockly.Workspace.getById('badId'));
});
test('After dispose', function() {
this.workspaceB.dispose();
chai.assert.isNull(Blockly.Workspace.getById(this.workspaceB.id));
});
});
suite('getBlockById', function() {
setup(function() {
this.blockA = this.workspace.newBlock('');
this.blockB = this.workspace.newBlock('');
this.workspaceB = this.workspace.rendered ?
new Blockly.WorkspaceSvg(new Blockly.Options({})) :
new Blockly.Workspace();
});
teardown(function() {
workspaceTeardown.call(this, this.workspaceB);
});
test('Trivial', function() {
chai.assert.equal(
this.workspace.getBlockById(this.blockA.id),this.blockA);
chai.assert.equal(
this.workspace.getBlockById(this.blockB.id), this.blockB);
});
test('Null id', function() {
chai.assert.isNull(this.workspace.getBlockById(null));
});
test('Non-existent id', function() {
chai.assert.isNull(this.workspace.getBlockById('badId'));
});
test('After dispose', function() {
this.blockA.dispose();
chai.assert.isNull(this.workspace.getBlockById(this.blockA.id));
chai.assert.equal(
this.workspace.getBlockById(this.blockB.id), this.blockB);
});
test('After clear', function() {
this.workspace.clear();
chai.assert.isNull(this.workspace.getBlockById(this.blockA.id));
chai.assert.isNull(this.workspace.getBlockById(this.blockB.id));
});
});
suite('Undo/Redo', function() {
/**
* Assert that two nodes are equal.
* @param {!Element} actual the actual node.
* @param {!Element} expected the expected node.
*/
function assertNodesEqual(actual, expected) {
var actualString = '\n' + Blockly.Xml.domToPrettyText(actual) + '\n';
var expectedString = '\n' + Blockly.Xml.domToPrettyText(expected) + '\n';
chai.assert.equal(actual.tagName, expected.tagName);
for (var i = 0, attr; (attr = expected.attributes[i]); i++) {
chai.assert.equal(actual.getAttribute(attr.name), attr.value,
`expected attribute ${attr.name} on ${actualString} to match ` +
`${expectedString}`);
}
chai.assert.equal(actual.childElementCount, expected.childElementCount,
`expected node ${actualString} to have the same children as node ` +
`${expectedString}`);
for (var i = 0; i < expected.childElementCount; i++) {
assertNodesEqual(actual.children[i], expected.children[i]);
}
}
suite('Undo Delete', function() {
setup(function() {
Blockly.defineBlocksWithJsonArray([
{
"type": "stack_block",
"message0": "",
"previousStatement": null,
"nextStatement": null
},
{
"type": "row_block",
"message0": "%1",
"args0": [
{
"type": "input_value",
"name": "INPUT"
}
],
"output": null
},
{
"type": "statement_block",
"message0": "%1",
"args0": [
{
"type": "input_statement",
"name": "STATEMENT"
}
],
"previousStatement": null,
"nextStatement": null
}]);
});
teardown(function() {
delete Blockly.Blocks['stack_block'];
delete Blockly.Blocks['row_block'];
delete Blockly.Blocks['statement_block'];
});
function testUndoDelete(xmlText) {
var xml = Blockly.Xml.textToDom(xmlText);
Blockly.Xml.domToBlock(xml, this.workspace);
this.workspace.getTopBlocks()[0].dispose(false);
this.workspace.undo();
var newXml = Blockly.Xml.workspaceToDom(this.workspace);
assertNodesEqual(newXml.firstChild, xml);
}
test('Stack', function() {
testUndoDelete.call(this, '<block type="stack_block" id="1"/>');
});
test('Row', function() {
testUndoDelete.call(this, '<block type="row_block" id="1"/>');
});
test('Statement', function() {
testUndoDelete.call(this, '<block type="statement_block" id="1"/>');
});
test('Stack w/ child', function() {
testUndoDelete.call(this,
'<block type="stack_block" id="1">' +
' <next>' +
' <block type="stack_block" id="2"></block>' +
' </next>' +
'</block>'
);
});
test('Row w/ child', function() {
testUndoDelete.call(this,
'<block type="row_block" id="1">' +
' <value name="INPUT">' +
' <block type="row_block" id="2"></block>' +
' </value>' +
'</block>'
);
});
test('Statement w/ child', function() {
testUndoDelete.call(this,
'<block type="statement_block" id="1">' +
' <statement name="STATEMENT">' +
' <block type="stack_block" id="2"></block>' +
' </statement>' +
'</block>'
);
});
test('Stack w/ shadow', function() {
testUndoDelete.call(this,
'<block type="stack_block" id="1">' +
' <next>' +
' <shadow type="stack_block" id="2"></shadow>' +
' </next>' +
'</block>'
);
});
test('Row w/ shadow', function() {
testUndoDelete.call(this,
'<block type="row_block" id="1">' +
' <value name="INPUT">' +
' <shadow type="row_block" id="2"></shadow>' +
' </value>' +
'</block>'
);
});
test('Statement w/ shadow', function() {
testUndoDelete.call(this,
'<block type="statement_block" id="1">' +
' <statement name="STATEMENT">' +
' <shadow type="stack_block" id="2"></shadow>' +
' </statement>' +
'</block>'
);
});
});
suite('Undo Connect', function() {
setup(function() {
Blockly.defineBlocksWithJsonArray([
{
"type": "stack_block",
"message0": "",
"previousStatement": null,
"nextStatement": null
},
{
"type": "row_block",
"message0": "%1",
"args0": [
{
"type": "input_value",
"name": "INPUT"
}
],
"output": null
},
{
"type": "statement_block",
"message0": "%1",
"args0": [
{
"type": "input_statement",
"name": "STATEMENT"
}
],
"previousStatement": null,
"nextStatement": null
}]);
});
teardown(function() {
delete Blockly.Blocks['stack_block'];
delete Blockly.Blocks['row_block'];
delete Blockly.Blocks['statement_block'];
});
function testUndoConnect(xmlText, parentId, childId, func) {
var xml = Blockly.Xml.textToDom(xmlText);
Blockly.Xml.domToWorkspace(xml, this.workspace);
var parent = this.workspace.getBlockById(parentId);
var child = this.workspace.getBlockById(childId);
func.call(this, parent, child);
this.workspace.undo();
var newXml = Blockly.Xml.workspaceToDom(this.workspace);
assertNodesEqual(newXml, xml);
}
test('Stack', function() {
var xml =
'<xml>' +
' <block type="stack_block" id="1"></block>' +
' <block type="stack_block" id="2"></block>' +
'</xml>';
testUndoConnect.call(this, xml, 1, 2, (parent, child) => {
parent.nextConnection.connect(child.previousConnection);
});
});
test('Row', function() {
var xml =
'<xml>' +
' <block type="row_block" id="1"></block>' +
' <block type="row_block" id="2"></block>' +
'</xml>';
testUndoConnect.call(this, xml, 1, 2, (parent, child) => {
parent.getInput('INPUT').connection.connect(child.outputConnection);
});
});
test('Statement', function() {
var xml =
'<xml>' +
' <block type="statement_block" id="1"></block>' +
' <block type="stack_block" id="2"></block>' +
'</xml>';
testUndoConnect.call(this, xml, 1, 2, (parent, child) => {
parent.getInput('STATEMENT').connection
.connect(child.previousConnection);
});
});
test('Stack w/ child', function() {
var xml =
'<xml>' +
' <block type="stack_block" id="1">' +
' <next>' +
' <block type="stack_block" id="3"></block>' +
' </next>' +
' </block>' +
' <block type="stack_block" id="2"></block>' +
'</xml>';
testUndoConnect.call(this, xml, 1, 2, (parent, child) => {
parent.nextConnection.connect(child.previousConnection);
});
});
test('Row w/ child', function() {
var xml =
'<xml>' +
' <block type="row_block" id="1">' +
' <value name="INPUT">' +
' <block type="row_block" id="3"></block>' +
' </value>' +
' </block>' +
' <block type="row_block" id="2"></block>' +
'</xml>';
testUndoConnect.call(this, xml, 1, 2, (parent, child) => {
parent.getInput('INPUT').connection.connect(child.outputConnection);
});
});
test('Statement w/ child', function() {
var xml =
'<xml>' +
' <block type="statement_block" id="1">' +
' <statement name="STATEMENT">' +
' <block type="stack_block" id="3"></block>' +
' </statement>' +
' </block>' +
' <block type="stack_block" id="2"></block>' +
'</xml>';
testUndoConnect.call(this, xml, 1, 2, (parent, child) => {
parent.getInput('STATEMENT').connection
.connect(child.previousConnection);
});
});
test('Stack w/ shadow', function() {
var xml =
'<xml>' +
' <block type="stack_block" id="1">' +
' <next>' +
' <shadow type="stack_block" id="3"></shadow>' +
' </next>' +
' </block>' +
' <block type="stack_block" id="2"></block>' +
'</xml>';
testUndoConnect.call(this, xml, 1, 2, (parent, child) => {
parent.nextConnection.connect(child.previousConnection);
});
});
test('Row w/ shadow', function() {
var xml =
'<xml>' +
' <block type="row_block" id="1">' +
' <value name="INPUT">' +
' <shadow type="row_block" id="3"></shadow>' +
' </value>' +
' </block>' +
' <block type="row_block" id="2"></block>' +
'</xml>';
testUndoConnect.call(this, xml, 1, 2, (parent, child) => {
parent.getInput('INPUT').connection.connect(child.outputConnection);
});
});
test('Statement w/ shadow', function() {
var xml =
'<xml>' +
' <block type="statement_block" id="1">' +
' <statement name="STATEMENT">' +
' <shadow type="stack_block" id="3"></shadow>' +
' </statement>' +
' </block>' +
' <block type="stack_block" id="2"></block>' +
'</xml>';
testUndoConnect.call(this, xml, 1, 2, (parent, child) => {
parent.getInput('STATEMENT').connection
.connect(child.previousConnection);
});
});
});
suite('Undo Disconnect', function() {
setup(function() {
Blockly.defineBlocksWithJsonArray([
{
"type": "stack_block",
"message0": "",
"previousStatement": null,
"nextStatement": null
},
{
"type": "row_block",
"message0": "%1",
"args0": [
{
"type": "input_value",
"name": "INPUT"
}
],
"output": null
},
{
"type": "statement_block",
"message0": "%1",
"args0": [
{
"type": "input_statement",
"name": "STATEMENT"
}
],
"previousStatement": null,
"nextStatement": null
}]);
});
teardown(function() {
delete Blockly.Blocks['stack_block'];
delete Blockly.Blocks['row_block'];
delete Blockly.Blocks['statement_block'];
});
function testUndoDisconnect(xmlText, childId) {
var xml = Blockly.Xml.textToDom(xmlText);
Blockly.Xml.domToWorkspace(xml, this.workspace);
var child = this.workspace.getBlockById(childId);
if (child.outputConnection) {
child.outputConnection.disconnect();
} else {
child.previousConnection.disconnect();
}
this.workspace.undo();
var newXml = Blockly.Xml.workspaceToDom(this.workspace);
assertNodesEqual(newXml, xml);
}
test('Stack', function() {
var xml =
'<xml>' +
' <block type="stack_block" id="1">' +
' <next>' +
' <block type="stack_block" id="2"></block>' +
' </next>' +
' </block>' +
'</xml>';
testUndoDisconnect.call(this, xml, 2);
});
test('Row', function() {
var xml =
'<xml>' +
' <block type="row_block" id="1">' +
' <value name="INPUT">' +
' <block type="row_block" id="2"></block>' +
' </value>' +
' </block>' +
'</xml>';
testUndoDisconnect.call(this, xml, 2);
});
test('Statement', function() {
var xml =
'<xml>' +
' <block type="statement_block" id="1">' +
' <statement name="STATEMENT">' +
' <block type="stack_block" id="2"></block>' +
' </statement>' +
' </block>' +
'</xml>';
testUndoDisconnect.call(this, xml, 2);
});
test('Stack w/ child', function() {
var xml =
'<xml>' +
' <block type="stack_block" id="1">' +
' <next>' +
' <block type="stack_block" id="2">' +
' <next>' +
' <block type="stack_block" id="3"></block>' +
' </next>' +
' </block>' +
' </next>' +
' </block>' +
'</xml>';
testUndoDisconnect.call(this, xml, 2);
});
test('Row w/ child', function() {
var xml =
'<xml>' +
' <block type="row_block" id="1">' +
' <value name="INPUT">' +
' <block type="row_block" id="2">' +
' <value name="INPUT">' +
' <block type="row_block" id="3"></block>' +
' </value>' +
' </block>' +
' </value>' +
' </block>' +
'</xml>';
testUndoDisconnect.call(this, xml, 2);
});
test('Statement w/ child', function() {
var xml =
'<xml>' +
' <block type="statement_block" id="1">' +
' <statement name="STATEMENT">' +
' <block type="statement_block" id="2">' +
' <statement name="STATEMENT">' +
' <block type="stack_block" id="3"></block>' +
' </statement>' +
' </block>' +
' </statement>' +
' </block>' +
'</xml>';
testUndoDisconnect.call(this, xml, 2);
});
test('Stack w/ shadow', function() {
// TODO: For some reason on next connections shadows are
// serialized second.
var xml =
'<xml>' +
' <block type="stack_block" id="1">' +
' <next>' +
' <block type="stack_block" id="2"></block>' +
' <shadow type="stack_block" id="3"></shadow>' +
' </next>' +
' </block>' +
'</xml>';
testUndoDisconnect.call(this, xml, 2);
chai.assert.equal(this.workspace.getAllBlocks().length, 2,
'expected there to only be 2 blocks on the workspace ' +
'(check for shadows)');
});
test('Row w/ shadow', function() {
var xml =
'<xml>' +
' <block type="row_block" id="1">' +
' <value name="INPUT">' +
' <shadow type="row_block" id="3"></shadow>' +
' <block type="row_block" id="2"></block>' +
' </value>' +
' </block>' +
'</xml>';
testUndoDisconnect.call(this, xml, 2);
chai.assert.equal(this.workspace.getAllBlocks().length, 2,
'expected there to only be 2 blocks on the workspace ' +
'(check for shadows)');
});
test('Statement w/ shadow', function() {
var xml =
'<xml>' +
' <block type="statement_block" id="1">' +
' <statement name="STATEMENT">' +
' <shadow type="stack_block" id="3"></shadow>' +
' <block type="stack_block" id="2"></block>' +
' </statement>' +
' </block>' +
'</xml>';
testUndoDisconnect.call(this, xml, 2);
});
});
suite('Variables', function() {
function createTwoVarsDifferentTypes(workspace) {
workspace.createVariable('name1', 'type1', 'id1');
workspace.createVariable('name2', 'type2', 'id2');
}
suite('createVariable', function() {
test('Undo only', function() {
createTwoVarsDifferentTypes(this.workspace);
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
chai.assert.isNull(this.workspace.getVariableById('id2'));
this.workspace.undo();
chai.assert.isNull(this.workspace.getVariableById('id1'));
chai.assert.isNull(this.workspace.getVariableById('id2'));
});
test('Undo and redo', function() {
createTwoVarsDifferentTypes(this.workspace);
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
chai.assert.isNull(this.workspace.getVariableById('id2'));
this.workspace.undo(true);
// Expect that variable 'id2' is recreated
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
this.workspace.undo();
this.workspace.undo();
chai.assert.isNull(this.workspace.getVariableById('id1'));
chai.assert.isNull(this.workspace.getVariableById('id2'));
this.workspace.undo(true);
// Expect that variable 'id1' is recreated
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
chai.assert.isNull(this.workspace.getVariableById('id2'));
});
});
suite('deleteVariableById', function() {
test('Undo only no usages', function() {
createTwoVarsDifferentTypes(this.workspace);
this.workspace.deleteVariableById('id1');
this.workspace.deleteVariableById('id2');
this.workspace.undo();
chai.assert.isNull(this.workspace.getVariableById('id1'));
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
});
test('Undo only with usages', function() {
createTwoVarsDifferentTypes(this.workspace);
// Create blocks to refer to both of them.
createVarBlocksNoEvents(this.workspace, ['id1', 'id2']);
this.workspace.deleteVariableById('id1');
this.workspace.deleteVariableById('id2');
this.workspace.undo();
assertBlockVarModelName(this.workspace, 0, 'name2');
chai.assert.isNull(this.workspace.getVariableById('id1'));
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
this.workspace.undo();
assertBlockVarModelName(this.workspace, 0, 'name2');
assertBlockVarModelName(this.workspace, 1, 'name1');
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
});
test('Reference exists no usages', function() {
createTwoVarsDifferentTypes(this.workspace);
this.workspace.deleteVariableById('id1');
this.workspace.deleteVariableById('id2');
this.workspace.undo();
chai.assert.isNull(this.workspace.getVariableById('id1'));
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
this.workspace.undo(true);
// Expect that both variables are deleted
chai.assert.isNull(this.workspace.getVariableById('id1'));
chai.assert.isNull(this.workspace.getVariableById('id2'));
this.workspace.undo();
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
this.workspace.undo(true);
// Expect that variable 'id2' is recreated
chai.assert.isNull(this.workspace.getVariableById('id1'));
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
});
test('Reference exists with usages', function() {
createTwoVarsDifferentTypes(this.workspace);
// Create blocks to refer to both of them.
createVarBlocksNoEvents(this.workspace, ['id1', 'id2']);
this.workspace.deleteVariableById('id1');
this.workspace.deleteVariableById('id2');
this.workspace.undo();
assertBlockVarModelName(this.workspace, 0, 'name2');
chai.assert.isNull(this.workspace.getVariableById('id1'));
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
this.workspace.undo(true);
// Expect that both variables are deleted
chai.assert.equal(this.workspace.topBlocks_.length, 0);
chai.assert.isNull(this.workspace.getVariableById('id1'));
chai.assert.isNull(this.workspace.getVariableById('id2'));
this.workspace.undo();
this.workspace.undo();
assertBlockVarModelName(this.workspace, 0, 'name2');
assertBlockVarModelName(this.workspace, 1, 'name1');
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
this.workspace.undo(true);
// Expect that variable 'id2' is recreated
assertBlockVarModelName(this.workspace,0, 'name2');
chai.assert.isNull(this.workspace.getVariableById('id1'));
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
});
test('Delete same variable twice no usages', function() {
this.workspace.createVariable('name1', 'type1', 'id1');
this.workspace.deleteVariableById('id1');
var workspace = this.workspace;
var warnings = captureWarnings(function() {
workspace.deleteVariableById('id1');
});
chai.assert.equal(warnings.length, 1,
'Expected 1 warning for second deleteVariableById call.');
// Check the undoStack only recorded one delete event.
var undoStack = this.workspace.undoStack_;
chai.assert.equal(undoStack[undoStack.length - 1].type, 'var_delete');
chai.assert.notEqual(undoStack[undoStack.length - 2].type, 'var_delete');
// Undo delete
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
// Redo delete
this.workspace.undo(true);
chai.assert.isNull(this.workspace.getVariableById('id1'));
// Redo delete, nothing should happen
this.workspace.undo(true);
chai.assert.isNull(this.workspace.getVariableById('id1'));
});
test('Delete same variable twice with usages', function() {
this.workspace.createVariable('name1', 'type1', 'id1');
createVarBlocksNoEvents(this.workspace, ['id1']);
this.workspace.deleteVariableById('id1');
var workspace = this.workspace;
var warnings = captureWarnings(function() {
workspace.deleteVariableById('id1');
});
chai.assert.equal(warnings.length, 1,
'Expected 1 warning for second deleteVariableById call.');
// Check the undoStack only recorded one delete event.
var undoStack = this.workspace.undoStack_;
chai.assert.equal(undoStack[undoStack.length - 1].type, 'var_delete');
chai.assert.equal(undoStack[undoStack.length - 2].type, 'delete');
chai.assert.notEqual(undoStack[undoStack.length - 3].type, 'var_delete');
// Undo delete
this.workspace.undo();
assertBlockVarModelName(this.workspace, 0, 'name1');
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
// Redo delete
this.workspace.undo(true);
chai.assert.equal(this.workspace.topBlocks_.length, 0);
chai.assert.isNull(this.workspace.getVariableById('id1'));
// Redo delete, nothing should happen
this.workspace.undo(true);
chai.assert.equal(this.workspace.topBlocks_.length, 0);
chai.assert.isNull(this.workspace.getVariableById('id1'));
});
});
suite('renameVariableById', function() {
setup(function() {
this.workspace.createVariable('name1', 'type1', 'id1');
});
test('Reference exists no usages rename to name2', function() {
this.workspace.renameVariableById('id1', 'name2');
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
this.workspace.undo(true);
assertVariableValues(this.workspace, 'name2', 'type1', 'id1');
});
test('Reference exists with usages rename to name2', function() {
createVarBlocksNoEvents(this.workspace, ['id1']);
this.workspace.renameVariableById('id1', 'name2');
this.workspace.undo();
assertBlockVarModelName(this.workspace, 0, 'name1');
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
this.workspace.undo(true);
assertBlockVarModelName(this.workspace, 0, 'name2');
assertVariableValues(this.workspace, 'name2', 'type1', 'id1');
});
test('Reference exists different capitalization no usages rename to Name1', function() {
this.workspace.renameVariableById('id1', 'Name1');
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
this.workspace.undo(true);
assertVariableValues(this.workspace, 'Name1', 'type1', 'id1');
});
test('Reference exists different capitalization with usages rename to Name1', function() {
createVarBlocksNoEvents(this.workspace, ['id1']);
this.workspace.renameVariableById('id1', 'Name1');
this.workspace.undo();
assertBlockVarModelName(this.workspace, 0, 'name1');
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
this.workspace.undo(true);
assertBlockVarModelName(this.workspace, 0, 'Name1');
assertVariableValues(this.workspace, 'Name1', 'type1', 'id1');
});
suite('Two variables rename overlap', function() {
test('Same type no usages rename variable with id1 to name2', function() {
this.workspace.createVariable('name2', 'type1', 'id2');
this.workspace.renameVariableById('id1', 'name2');
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type1', 'id2');
this.workspace.undo(true);
assertVariableValues(this.workspace, 'name2', 'type1', 'id2');
chai.assert.isNull(this.workspace.getVariableById('id1'));
});
test('Same type with usages rename variable with id1 to name2', function() {
this.workspace.createVariable('name2', 'type1', 'id2');
createVarBlocksNoEvents(this.workspace, ['id1', 'id2']);
this.workspace.renameVariableById('id1', 'name2');
this.workspace.undo();
assertBlockVarModelName(this.workspace, 0, 'name1');
assertBlockVarModelName(this.workspace, 1, 'name2');
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type1', 'id2');
this.workspace.undo(true);
assertVariableValues(this.workspace, 'name2', 'type1', 'id2');
chai.assert.isNull(this.workspace.getVariableById('id1'));
});
test('Same type different capitalization no usages rename variable with id1 to Name2', function() {
this.workspace.createVariable('name2', 'type1', 'id2');
this.workspace.renameVariableById('id1', 'Name2');
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type1', 'id2');
this.workspace.undo(true);
assertVariableValues(this.workspace, 'Name2', 'type1', 'id2');
chai.assert.isNull(this.workspace.getVariable('name1'));
});
test('Same type different capitalization with usages rename variable with id1 to Name2', function() {
this.workspace.createVariable('name2', 'type1', 'id2');
createVarBlocksNoEvents(this.workspace, ['id1', 'id2']);
this.workspace.renameVariableById('id1', 'Name2');
this.workspace.undo();
assertBlockVarModelName(this.workspace, 0, 'name1');
assertBlockVarModelName(this.workspace, 1, 'name2');
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type1', 'id2');
this.workspace.undo(true);
assertVariableValues(this.workspace, 'Name2', 'type1', 'id2');
chai.assert.isNull(this.workspace.getVariableById('id1'));
assertBlockVarModelName(this.workspace, 0, 'Name2');
assertBlockVarModelName(this.workspace, 1, 'Name2');
});
test('Different type no usages rename variable with id1 to name2', function() {
this.workspace.createVariable('name2', 'type2', 'id2');
this.workspace.renameVariableById('id1', 'name2');
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
this.workspace.undo(true);
assertVariableValues(this.workspace, 'name2', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
});
test('Different type with usages rename variable with id1 to name2', function() {
this.workspace.createVariable('name2', 'type2', 'id2');
createVarBlocksNoEvents(this.workspace, ['id1', 'id2']);
this.workspace.renameVariableById('id1', 'name2');
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
assertBlockVarModelName(this.workspace, 0, 'name1');
assertBlockVarModelName(this.workspace, 1, 'name2');
this.workspace.undo(true);
assertVariableValues(this.workspace, 'name2', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
assertBlockVarModelName(this.workspace, 0, 'name2');
assertBlockVarModelName(this.workspace, 1, 'name2');
});
test('Different type different capitalization no usages rename variable with id1 to Name2', function() {
this.workspace.createVariable('name2', 'type2', 'id2');
this.workspace.renameVariableById('id1', 'Name2');
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
this.workspace.undo(true);
assertVariableValues(this.workspace, 'Name2', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
});
test('Different type different capitalization with usages rename variable with id1 to Name2', function() {
this.workspace.createVariable('name2', 'type2', 'id2');
createVarBlocksNoEvents(this.workspace, ['id1', 'id2']);
this.workspace.renameVariableById('id1', 'Name2');
this.workspace.undo();
assertVariableValues(this.workspace, 'name1', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
assertBlockVarModelName(this.workspace, 0, 'name1');
assertBlockVarModelName(this.workspace, 1, 'name2');
this.workspace.undo(true);
assertVariableValues(this.workspace, 'Name2', 'type1', 'id1');
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
assertBlockVarModelName(this.workspace, 0, 'Name2');
assertBlockVarModelName(this.workspace, 1, 'name2');
});
});
});
});
});
}