/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.module('Blockly.test.event');
const eventUtils = goog.require('Blockly.Events.utils');
const {assertEventEquals, assertNthCallEventArgEquals, assertVariableValues, createFireChangeListenerSpy, createGenUidStubWithReturns, sharedTestSetup, sharedTestTeardown, workspaceTeardown} = goog.require('Blockly.test.helpers');
const {ASTNode} = goog.require('Blockly.ASTNode');
goog.require('Blockly.WorkspaceComment');
suite('Events', function() {
setup(function() {
sharedTestSetup.call(this, {fireEventsNow: false});
this.eventsFireSpy = sinon.spy(eventUtils, 'fire');
this.workspace = new Blockly.Workspace();
Blockly.defineBlocksWithJsonArray([{
'type': 'field_variable_test_block',
'message0': '%1',
'args0': [
{
'type': 'field_variable',
'name': 'VAR',
'variable': 'item'
}
],
},
{
'type': 'simple_test_block',
'message0': 'simple test block'
}]);
});
teardown(function() {
sharedTestTeardown.call(this);
});
function createSimpleTestBlock(workspace) {
// Disable events while constructing the block: this is a test of the
// Blockly.Event constructors, not the block constructors.
// Set the group id to avoid an extra call to genUid.
eventUtils.disable();
let block;
try {
eventUtils.setGroup('unused');
block = new Blockly.Block(
workspace, 'simple_test_block');
} finally {
eventUtils.setGroup(false);
}
eventUtils.enable();
return block;
}
suite('Constructors', function() {
test('Abstract', function() {
let event = new Blockly.Events.Abstract();
assertEventEquals(event, undefined, undefined, undefined, {
'recordUndo': true,
'group': ''
});
});
test('UI event without block', function() {
let event = new Blockly.Events.UiBase(this.workspace.id);
assertEventEquals(event, undefined, this.workspace.id, undefined, {
'recordUndo': false,
'group': '',
}, true);
});
test('Click without block', function() {
let event = new Blockly.Events.Click(null, this.workspace.id, 'workspace');
assertEventEquals(event, Blockly.Events.CLICK, this.workspace.id, null, {
'targetType': 'workspace',
'recordUndo': false,
'group': ''
}, true);
});
test('Old UI event without block', function() {
let TEST_GROUP_ID = 'testGroup';
eventUtils.setGroup(TEST_GROUP_ID);
let event = new Blockly.Events.Ui(null, 'foo', 'bar', 'baz');
assertEventEquals(event, Blockly.Events.UI, '', null, {
'element': 'foo',
'oldValue': 'bar',
'newValue': 'baz',
'recordUndo': false,
'group': TEST_GROUP_ID
}, true);
});
suite('With simple blocks', function() {
setup(function() {
this.TEST_BLOCK_ID = 'test_block_id';
this.TEST_PARENT_ID = 'parent';
// genUid is expected to be called either once or twice in this suite.
this.genUidStub = createGenUidStubWithReturns(
[this.TEST_BLOCK_ID, this.TEST_PARENT_ID]);
this.block = createSimpleTestBlock(this.workspace);
});
test('Block base', function() {
let event = new Blockly.Events.BlockBase(this.block);
sinon.assert.calledOnce(this.genUidStub);
assertEventEquals(event, undefined,
this.workspace.id, this.TEST_BLOCK_ID,
{
'varId': undefined,
'recordUndo': true,
'group': '',
});
});
test('Block create', function() {
let event = new Blockly.Events.BlockCreate(this.block);
sinon.assert.calledOnce(this.genUidStub);
assertEventEquals(event, Blockly.Events.BLOCK_CREATE,
this.workspace.id, this.TEST_BLOCK_ID,
{
'recordUndo': true,
'group': '',
});
});
test('Block delete', function() {
let event = new Blockly.Events.BlockDelete(this.block);
sinon.assert.calledOnce(this.genUidStub);
assertEventEquals(event, Blockly.Events.BLOCK_DELETE,
this.workspace.id, this.TEST_BLOCK_ID,
{
'recordUndo': true,
'group': '',
});
});
test('Old UI event with block', function() {
let TEST_GROUP_ID = 'testGroup';
eventUtils.setGroup(TEST_GROUP_ID);
let event = new Blockly.Events.Ui(this.block, 'foo', 'bar', 'baz');
sinon.assert.calledOnce(this.genUidStub);
assertEventEquals(event, Blockly.Events.UI, this.workspace.id,
this.TEST_BLOCK_ID,
{
'element': 'foo',
'oldValue': 'bar',
'newValue': 'baz',
'recordUndo': false,
'group': TEST_GROUP_ID
}, true);
});
test('Click with block', function() {
let TEST_GROUP_ID = 'testGroup';
eventUtils.setGroup(TEST_GROUP_ID);
let event = new Blockly.Events.Click(this.block, null, 'block');
assertEventEquals(event, Blockly.Events.CLICK, this.workspace.id,
this.TEST_BLOCK_ID, {
'targetType': 'block',
'recordUndo': false,
'group': TEST_GROUP_ID
}, true);
});
suite('Block Move', function() {
test('by coordinate', function() {
let coordinate = new Blockly.utils.Coordinate(3, 4);
this.block.xy_ = coordinate;
let event = new Blockly.Events.BlockMove(this.block);
sinon.assert.calledOnce(this.genUidStub);
assertEventEquals(event, Blockly.Events.BLOCK_MOVE, this.workspace.id,
this.TEST_BLOCK_ID, {
'oldParentId': undefined,
'oldInputName': undefined,
'oldCoordinate': coordinate,
'recordUndo': true,
'group': ''
});
});
test('by parent', function() {
try {
this.parentBlock = createSimpleTestBlock(this.workspace);
this.block.parentBlock_ = this.parentBlock;
this.block.xy_ = new Blockly.utils.Coordinate(3, 4);
let event = new Blockly.Events.BlockMove(this.block);
sinon.assert.calledTwice(this.genUidStub);
assertEventEquals(event, Blockly.Events.BLOCK_MOVE, this.workspace.id,
this.TEST_BLOCK_ID, {
'oldParentId': this.TEST_PARENT_ID,
'oldInputName': undefined,
'oldCoordinate': undefined,
'recordUndo': true,
'group': ''
});
} finally {
// This needs to be cleared, otherwise workspace.dispose will fail.
this.block.parentBlock_ = null;
}
});
});
});
suite('With shadow blocks', function() {
setup(function() {
this.TEST_BLOCK_ID = 'test_block_id';
this.TEST_PARENT_ID = 'parent';
// genUid is expected to be called either once or twice in this suite.
this.genUidStub = createGenUidStubWithReturns(
[this.TEST_BLOCK_ID, this.TEST_PARENT_ID]);
this.block = createSimpleTestBlock(this.workspace);
this.block.setShadow(true);
});
test('Block base', function() {
let event = new Blockly.Events.BlockBase(this.block);
sinon.assert.calledOnce(this.genUidStub);
assertEventEquals(event, undefined,
this.workspace.id, this.TEST_BLOCK_ID,
{
'varId': undefined,
'recordUndo': true,
'group': '',
});
});
test('Block change', function() {
let event = new Blockly.Events.BlockChange(
this.block, 'field', 'FIELD_NAME', 'old', 'new');
sinon.assert.calledOnce(this.genUidStub);
assertEventEquals(event, Blockly.Events.BLOCK_CHANGE,
this.workspace.id, this.TEST_BLOCK_ID,
{
'varId': undefined,
'element': 'field',
'name': 'FIELD_NAME',
'oldValue': 'old',
'newValue': 'new',
'recordUndo': true,
'group': '',
});
});
test('Block create', function() {
let event = new Blockly.Events.BlockCreate(this.block);
sinon.assert.calledOnce(this.genUidStub);
assertEventEquals(event, Blockly.Events.BLOCK_CREATE,
this.workspace.id, this.TEST_BLOCK_ID,
{
'recordUndo': false,
'group': '',
});
});
test('Block delete', function() {
let event = new Blockly.Events.BlockDelete(this.block);
sinon.assert.calledOnce(this.genUidStub);
assertEventEquals(event, Blockly.Events.BLOCK_DELETE,
this.workspace.id, this.TEST_BLOCK_ID,
{
'recordUndo': false,
'group': '',
});
});
test('Block move', function() {
try {
this.parentBlock = createSimpleTestBlock(this.workspace);
this.block.parentBlock_ = this.parentBlock;
this.block.xy_ = new Blockly.utils.Coordinate(3, 4);
let event = new Blockly.Events.BlockMove(this.block);
sinon.assert.calledTwice(this.genUidStub);
assertEventEquals(event, Blockly.Events.BLOCK_MOVE, this.workspace.id,
this.TEST_BLOCK_ID,
{
'oldParentId': this.TEST_PARENT_ID,
'oldInputName': undefined,
'oldCoordinate': undefined,
'recordUndo': false,
'group': ''
});
} finally {
// This needs to be cleared, otherwise workspace.dispose will fail.
this.block.parentBlock_ = null;
}
});
});
suite('With variable getter blocks', function() {
setup(function() {
this.genUidStub = createGenUidStubWithReturns(
[this.TEST_BLOCK_ID, 'test_var_id', 'test_group_id']);
// Disabling events when creating a block with variable can cause issues
// at workspace dispose.
this.block = new Blockly.Block(
this.workspace, 'field_variable_test_block');
});
test('Block change', function() {
let event = new Blockly.Events.BlockChange(
this.block, 'field', 'VAR', 'id1', 'id2');
assertEventEquals(event, Blockly.Events.BLOCK_CHANGE, this.workspace.id,
this.TEST_BLOCK_ID,
{
'element': 'field',
'name': 'VAR',
'oldValue': 'id1',
'newValue': 'id2',
'recordUndo': true,
'group': ''
});
});
});
});
suite('Serialization', function() {
let safeStringify = (json) => {
let cache = [];
return JSON.stringify(json, (key, value) => {
if (typeof value == 'object' && value != null) {
if (cache.includes(value)) {
// Discard duplicate reference.
return undefined;
}
cache.push(value);
return value;
}
return value;
});
};
let variableEventTestCases = [
{title: 'Var create', class: Blockly.Events.VarCreate,
getArgs: (thisObj) => [thisObj.variable],
getExpectedJson: () => ({type: 'var_create', varId: 'id1',
varType: 'type1', varName: 'name1'})},
{title: 'Var delete', class: Blockly.Events.VarDelete,
getArgs: (thisObj) => [thisObj.variable],
getExpectedJson: () => ({type: 'var_delete', varId: 'id1',
varType: 'type1', varName: 'name1'})},
{title: 'Var rename', class: Blockly.Events.VarRename,
getArgs: (thisObj) => [thisObj.variable, 'name2'],
getExpectedJson: () => ({type: 'var_rename', varId: 'id1',
oldName: 'name1', newName: 'name2'})},
];
let uiEventTestCases = [
{title: 'Bubble open', class: Blockly.Events.BubbleOpen,
getArgs: (thisObj) => [thisObj.block, true, 'mutator'],
getExpectedJson: (thisObj) => ({type: 'bubble_open', isOpen: true,
bubbleType: 'mutator', blockId: thisObj.block.id})},
{title: 'Block click', class: Blockly.Events.Click,
getArgs: (thisObj) => [thisObj.block, null, 'block'],
getExpectedJson: (thisObj) => ({type: 'click', targetType: 'block',
blockId: thisObj.block.id})},
{title: 'Workspace click', class: Blockly.Events.Click,
getArgs: (thisObj) => [null, thisObj.workspace.id, 'workspace'],
getExpectedJson: (thisObj) => ({type: 'click',
targetType: 'workspace'})},
{title: 'Drag start', class: Blockly.Events.BlockDrag,
getArgs: (thisObj) => [thisObj.block, true, [thisObj.block]],
getExpectedJson: (thisObj) => ({type: 'drag',
isStart: true, blockId: thisObj.block.id, blocks: [thisObj.block]})},
{title: 'Drag end', class: Blockly.Events.BlockDrag,
getArgs: (thisObj) => [thisObj.block, false, [thisObj.block]],
getExpectedJson: (thisObj) => ({type: 'drag',
isStart: false, blockId: thisObj.block.id, blocks: [thisObj.block]})},
{title: 'null to Block Marker move', class: Blockly.Events.MarkerMove,
getArgs: (thisObj) => [thisObj.block, true, null,
new ASTNode(ASTNode.types.BLOCK, thisObj.block)],
getExpectedJson: (thisObj) => ({type: 'marker_move',
isCursor: true, blockId: thisObj.block.id, oldNode: null,
newNode: new ASTNode(ASTNode.types.BLOCK,
thisObj.block)})},
{title: 'null to Workspace Marker move', class: Blockly.Events.MarkerMove,
getArgs: (thisObj) => [null, true, null,
ASTNode.createWorkspaceNode(thisObj.workspace,
new Blockly.utils.Coordinate(0, 0))],
getExpectedJson: (thisObj) => ({type: 'marker_move',
isCursor: true, blockId: null, oldNode: null,
newNode: ASTNode.createWorkspaceNode(thisObj.workspace,
new Blockly.utils.Coordinate(0, 0))})},
{title: 'Workspace to Block Marker move',
class: Blockly.Events.MarkerMove,
getArgs: (thisObj) => [thisObj.block, true,
ASTNode.createWorkspaceNode(thisObj.workspace,
new Blockly.utils.Coordinate(0, 0)),
new ASTNode(ASTNode.types.BLOCK, thisObj.block)],
getExpectedJson: (thisObj) => ({type: 'marker_move',
isCursor: true, blockId: thisObj.block.id,
oldNode: ASTNode.createWorkspaceNode(thisObj.workspace,
new Blockly.utils.Coordinate(0, 0)),
newNode: new ASTNode(ASTNode.types.BLOCK,
thisObj.block)})},
{title: 'Block to Workspace Marker move',
class: Blockly.Events.MarkerMove,
getArgs: (thisObj) => [null, true,
new ASTNode(ASTNode.types.BLOCK, thisObj.block),
ASTNode.createWorkspaceNode(thisObj.workspace,
new Blockly.utils.Coordinate(0, 0))]},
{title: 'Selected', class: Blockly.Events.Selected,
getArgs: (thisObj) => [null, thisObj.block.id, thisObj.workspace.id],
getExpectedJson: (thisObj) => ({type: 'selected', oldElementId: null,
newElementId: thisObj.block.id})},
{title: 'Selected (deselect)', class: Blockly.Events.Selected,
getArgs: (thisObj) => [thisObj.block.id, null, thisObj.workspace.id],
getExpectedJson: (thisObj) => ({type: 'selected',
oldElementId: thisObj.block.id, newElementId: null})},
{title: 'Theme Change', class: Blockly.Events.ThemeChange,
getArgs: (thisObj) => ['classic', thisObj.workspace.id],
getExpectedJson: () => ({type: 'theme_change', themeName: 'classic'})},
{title: 'Toolbox item select',
class: Blockly.Events.ToolboxItemSelect,
getArgs: (thisObj) => ['Math', 'Loops', thisObj.workspace.id],
getExpectedJson: () => ({type: 'toolbox_item_select', oldItem: 'Math',
newItem: 'Loops'})},
{title: 'Toolbox item select (no previous)',
class: Blockly.Events.ToolboxItemSelect,
getArgs: (thisObj) => [null, 'Loops', thisObj.workspace.id],
getExpectedJson: () => ({type: 'toolbox_item_select', oldItem: null,
newItem: 'Loops'})},
{title: 'Toolbox item select (deselect)',
class: Blockly.Events.ToolboxItemSelect,
getArgs: (thisObj) => ['Math', null, thisObj.workspace.id],
getExpectedJson: () => ({type: 'toolbox_item_select', oldItem: 'Math',
newItem: null})},
{title: 'Trashcan open', class: Blockly.Events.TrashcanOpen,
getArgs: (thisObj) => [true, thisObj.workspace.id],
getExpectedJson: () => ({type: 'trashcan_open', isOpen: true})},
{title: 'Viewport change', class: Blockly.Events.ViewportChange,
getArgs: (thisObj) => [2.666, 1.333, 1.2, thisObj.workspace.id, 1],
getExpectedJson: () => ({type: 'viewport_change', viewTop: 2.666,
viewLeft: 1.333, scale: 1.2, oldScale: 1})},
{title: 'Viewport change (0,0)', class: Blockly.Events.ViewportChange,
getArgs: (thisObj) => [0, 0, 1.2, thisObj.workspace.id, 1],
getExpectedJson: () => ({type: 'viewport_change', viewTop: 0,
viewLeft: 0, scale: 1.2, oldScale: 1})},
];
let blockEventTestCases = [
{
title: 'Block change',
class: Blockly.Events.BlockChange,
getArgs: (thisObj) => [thisObj.block, 'collapsed', null, false, true],
getExpectedJson: (thisObj) => ({
type: 'change',
blockId: thisObj.block.id,
element: 'collapsed',
oldValue: false,
newValue: true
})
},
{
title: 'Block create',
class: Blockly.Events.BlockCreate,
getArgs: (thisObj) => [thisObj.block],
getExpectedJson: (thisObj) => ({
type: 'create',
blockId: thisObj.block.id,
xml: '' +
'',
ids: [thisObj.block.id],
json: {
'type': 'simple_test_block',
'id': 'testBlockId1',
'x': 0,
'y': 0,
},
})
},
{
title: 'Block create (shadow)',
class: Blockly.Events.BlockCreate,
getArgs: (thisObj) => [thisObj.shadowBlock],
getExpectedJson: (thisObj) => ({
type: 'create',
blockId: thisObj.shadowBlock.id,
xml: '' +
'',
ids: [thisObj.shadowBlock.id],
json: {
'type': 'simple_test_block',
'id': 'testBlockId2',
'x': 0,
'y': 0,
},
recordUndo: false
})
},
{
title: 'Block delete',
class: Blockly.Events.BlockDelete,
getArgs: (thisObj) => [thisObj.block],
getExpectedJson: (thisObj) => ({
type: 'delete',
blockId: thisObj.block.id,
oldXml: '' +
'',
ids: [thisObj.block.id],
wasShadow: false,
oldJson: {
'type': 'simple_test_block',
'id': 'testBlockId1',
'x': 0,
'y': 0,
},
})
},
{
title: 'Block delete (shadow)',
class: Blockly.Events.BlockDelete,
getArgs: (thisObj) => [thisObj.shadowBlock],
getExpectedJson: (thisObj) => ({
type: 'delete',
blockId: thisObj.shadowBlock.id,
oldXml: '' +
'',
ids: [thisObj.shadowBlock.id],
wasShadow: true,
oldJson: {
'type': 'simple_test_block',
'id': 'testBlockId2',
'x': 0,
'y': 0,
},
recordUndo: false
})
},
// TODO(#4577) Test serialization of move event coordinate properties.
{
title: 'Block move',
class: Blockly.Events.BlockMove,
getArgs: (thisObj) => [thisObj.block],
getExpectedJson: (thisObj) => ({
type: 'move',
blockId: thisObj.block.id
})
},
{
title: 'Block move (shadow)',
class: Blockly.Events.BlockMove,
getArgs: (thisObj) => [thisObj.shadowBlock],
getExpectedJson: (thisObj) => ({
type: 'move',
blockId: thisObj.shadowBlock.id,
recordUndo: false
})
},
];
let workspaceEventTestCases = [
{title: 'Finished Loading', class: Blockly.Events.FinishedLoading,
getArgs: (thisObj) => [thisObj.workspace],
getExpectedJson: (thisObj) => ({type: 'finished_loading',
workspaceId: thisObj.workspace.id})},
];
let workspaceCommentEventTestCases = [
{title: 'Comment change', class: Blockly.Events.CommentChange,
getArgs: (thisObj) => [thisObj.comment, 'bar', 'foo'],
getExpectedJson: (thisObj) => ({type: 'comment_change',
commentId: thisObj.comment.id, oldContents: 'bar',
newContents: 'foo'})},
{title: 'Comment create', class: Blockly.Events.CommentCreate,
getArgs: (thisObj) => [thisObj.comment],
getExpectedJson: (thisObj) => ({type: 'comment_create',
commentId: thisObj.comment.id,
xml: Blockly.Xml.domToText(thisObj.comment.toXmlWithXY())})},
{title: 'Comment delete', class: Blockly.Events.CommentDelete,
getArgs: (thisObj) => [thisObj.comment],
getExpectedJson: (thisObj) => ({type: 'comment_delete',
commentId: thisObj.comment.id})},
// TODO(#4577) Test serialization of move event coordinate properties.
{title: 'Comment move', class: Blockly.Events.CommentMove,
getArgs: (thisObj) => [thisObj.comment],
getExpectedJson: (thisObj) => ({type: 'comment_move',
commentId: thisObj.comment.id, oldCoordinate: '0,0'})},
];
let testSuites = [
{title: 'Variable events', testCases: variableEventTestCases,
setup: (thisObj) => {
thisObj.variable =
thisObj.workspace.createVariable('name1', 'type1', 'id1');
}},
{title: 'UI events', testCases: uiEventTestCases,
setup: (thisObj) => {
thisObj.block = createSimpleTestBlock(thisObj.workspace);
}},
{title: 'Block events', testCases: blockEventTestCases,
setup: (thisObj) => {
createGenUidStubWithReturns(['testBlockId1', 'testBlockId2']);
thisObj.block = createSimpleTestBlock(thisObj.workspace);
thisObj.shadowBlock = createSimpleTestBlock(thisObj.workspace);
thisObj.shadowBlock.setShadow(true);
}},
{title: 'Workspace events',
testCases: workspaceEventTestCases,
setup: (_) => {}},
{title: 'WorkspaceComment events',
testCases: workspaceCommentEventTestCases,
setup: (thisObj) => {
thisObj.comment = new Blockly.WorkspaceComment(
thisObj.workspace, 'comment text', 0, 0, 'comment id');
}},
];
testSuites.forEach((testSuite) => {
suite(testSuite.title, function() {
setup(function() {
testSuite.setup(this);
});
suite('fromJson', function() {
testSuite.testCases.forEach((testCase) => {
test(testCase.title, function() {
let event = new testCase.class(...testCase.getArgs(this));
let event2 = new testCase.class();
let json = event.toJson();
event2.fromJson(json);
chai.assert.equal(
safeStringify(event2.toJson()), safeStringify(json));
});
});
});
suite('toJson', function() {
testSuite.testCases.forEach((testCase) => {
if (testCase.getExpectedJson) {
test(testCase.title, function() {
let event = new testCase.class(...testCase.getArgs(this));
let json = event.toJson();
let expectedJson = testCase.getExpectedJson(this);
chai.assert.equal(
safeStringify(json), safeStringify(expectedJson));
});
}
});
});
});
});
});
suite('Variable events', function() {
setup(function() {
this.variable = this.workspace.createVariable('name1', 'type1', 'id1');
});
/**
* Check if a variable with the given values exists.
* @param {Blockly.Workspace|Blockly.VariableMap} container The workspace or
* variableMap the checked variable belongs to.
* @param {!string} name The expected name of the variable.
* @param {!string} type The expected type of the variable.
* @param {!string} id The expected id of the variable.
*/
function checkVariableValues(container, name, type, id) {
let variable = container.getVariableById(id);
chai.assert.isDefined(variable);
chai.assert.equal(name, variable.name);
chai.assert.equal(type, variable.type);
chai.assert.equal(id, variable.getId());
}
suite('Constructors', function() {
test('Var base', function() {
let event = new Blockly.Events.VarBase(this.variable);
assertEventEquals(event, undefined, this.workspace.id, undefined, {
'varId': 'id1',
'recordUndo': true,
'group': ''
});
});
test('Var create', function() {
let event = new Blockly.Events.VarCreate(this.variable);
assertEventEquals(event, Blockly.Events.VAR_CREATE, this.workspace.id,
undefined,
{
'varId': 'id1',
'varType': 'type1',
'varName': 'name1',
'recordUndo': true,
'group': ''
});
});
test('Var delete', function() {
let event = new Blockly.Events.VarDelete(this.variable);
assertEventEquals(event, Blockly.Events.VAR_DELETE, this.workspace.id,
undefined,
{
'varId': 'id1',
'varType': 'type1',
'varName': 'name1',
'recordUndo': true,
'group': ''
});
});
test('Var rename', function() {
let event = new Blockly.Events.VarRename(this.variable, 'name2');
assertEventEquals(event, Blockly.Events.VAR_RENAME, this.workspace.id,
undefined,
{
'varId': 'id1',
'oldName': 'name1',
'newName': 'name2',
'recordUndo': true,
'group': ''
});
});
});
suite('Run Forward', function() {
test('Var create', function() {
let json = {type: "var_create", varId: "id2", varType: "type2",
varName: "name2"};
let event = eventUtils.fromJson(json, this.workspace);
let x = this.workspace.getVariableById('id2');
chai.assert.isNull(x);
event.run(true);
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
});
test('Var delete', function() {
let event = new Blockly.Events.VarDelete(this.variable);
chai.assert.isNotNull(this.workspace.getVariableById('id1'));
event.run(true);
chai.assert.isNull(this.workspace.getVariableById('id1'));
});
test('Var rename', function() {
let event = new Blockly.Events.VarRename(this.variable, 'name2');
event.run(true);
chai.assert.isNull(this.workspace.getVariable('name1'));
checkVariableValues(this.workspace, 'name2', 'type1', 'id1');
});
});
suite('Run Backward', function() {
test('Var create', function() {
let event = new Blockly.Events.VarCreate(this.variable);
chai.assert.isNotNull(this.workspace.getVariableById('id1'));
event.run(false);
});
test('Var delete', function() {
let json = {type: "var_delete", varId: "id2", varType: "type2",
varName: "name2"};
let event = eventUtils.fromJson(json, this.workspace);
chai.assert.isNull(this.workspace.getVariableById('id2'));
event.run(false);
assertVariableValues(this.workspace, 'name2', 'type2', 'id2');
});
test('Var rename', function() {
let event = new Blockly.Events.VarRename(this.variable, 'name2');
event.run(false);
chai.assert.isNull(this.workspace.getVariable('name2'));
checkVariableValues(this.workspace, 'name1', 'type1', 'id1');
});
});
});
suite('Filters', function() {
function addMoveEvent(events, block, newX, newY) {
events.push(new Blockly.Events.BlockMove(block));
block.xy_ = new Blockly.utils.Coordinate(newX, newY);
events[events.length - 1].recordNew();
}
function addMoveEventParent(events, block, parent) {
events.push(new Blockly.Events.BlockMove(block));
block.setParent(parent);
events[events.length - 1].recordNew();
}
test('No removed, order unchanged', function() {
let block = this.workspace.newBlock('field_variable_test_block', '1');
let events = [
new Blockly.Events.BlockCreate(block),
new Blockly.Events.BlockMove(block),
new Blockly.Events.BlockChange(block, 'field', 'VAR', 'id1', 'id2'),
new Blockly.Events.Click(block)
];
let filteredEvents = eventUtils.filter(events, true);
chai.assert.equal(filteredEvents.length, 4); // no event should have been removed.
// test that the order hasn't changed
chai.assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate);
chai.assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
chai.assert.isTrue(filteredEvents[2] instanceof Blockly.Events.BlockChange);
chai.assert.isTrue(filteredEvents[3] instanceof Blockly.Events.Click);
});
test('Different blocks no removed', function() {
let block1 = this.workspace.newBlock('field_variable_test_block', '1');
let block2 = this.workspace.newBlock('field_variable_test_block', '2');
let events = [
new Blockly.Events.BlockCreate(block1),
new Blockly.Events.BlockMove(block1),
new Blockly.Events.BlockCreate(block2),
new Blockly.Events.BlockMove(block2)
];
let filteredEvents = eventUtils.filter(events, true);
chai.assert.equal(filteredEvents.length, 4); // no event should have been removed.
});
test('Forward', function() {
let block = this.workspace.newBlock('field_variable_test_block', '1');
let events = [ new Blockly.Events.BlockCreate(block) ];
addMoveEvent(events, block, 1, 1);
addMoveEvent(events, block, 2, 2);
addMoveEvent(events, block, 3, 3);
let filteredEvents = eventUtils.filter(events, true);
chai.assert.equal(filteredEvents.length, 2); // duplicate moves should have been removed.
// test that the order hasn't changed
chai.assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate);
chai.assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
chai.assert.equal(filteredEvents[1].newCoordinate.x, 3);
chai.assert.equal(filteredEvents[1].newCoordinate.y, 3);
});
test('Backward', function() {
let block = this.workspace.newBlock('field_variable_test_block', '1');
let events = [ new Blockly.Events.BlockCreate(block) ];
addMoveEvent(events, block, 1, 1);
addMoveEvent(events, block, 2, 2);
addMoveEvent(events, block, 3, 3);
let filteredEvents = eventUtils.filter(events, false);
chai.assert.equal(filteredEvents.length, 2); // duplicate event should have been removed.
// test that the order hasn't changed
chai.assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate);
chai.assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
chai.assert.equal(filteredEvents[1].newCoordinate.x, 1);
chai.assert.equal(filteredEvents[1].newCoordinate.y, 1);
});
test('Merge block move events', function() {
let block = this.workspace.newBlock('field_variable_test_block', '1');
let events = [];
addMoveEvent(events, block, 0, 0);
addMoveEvent(events, block, 1, 1);
let filteredEvents = eventUtils.filter(events, true);
chai.assert.equal(filteredEvents.length, 1); // second move event merged into first
chai.assert.equal(filteredEvents[0].newCoordinate.x, 1);
chai.assert.equal(filteredEvents[0].newCoordinate.y, 1);
});
test('Merge block change events', function() {
let block1 = this.workspace.newBlock('field_variable_test_block', '1');
let events = [
new Blockly.Events.BlockChange(block1, 'field', 'VAR', 'item', 'item1'),
new Blockly.Events.BlockChange(block1, 'field', 'VAR', 'item1', 'item2')
];
let filteredEvents = eventUtils.filter(events, true);
chai.assert.equal(filteredEvents.length, 1); // second change event merged into first
chai.assert.equal(filteredEvents[0].oldValue, 'item');
chai.assert.equal(filteredEvents[0].newValue, 'item2');
});
test('Merge viewport change events', function() {
let events = [
new Blockly.Events.ViewportChange(1, 2, 3, this.workspace, 4),
new Blockly.Events.ViewportChange(5, 6, 7, this.workspace, 8)
];
let filteredEvents = eventUtils.filter(events, true);
chai.assert.equal(filteredEvents.length, 1); // second change event merged into first
chai.assert.equal(filteredEvents[0].viewTop, 5);
chai.assert.equal(filteredEvents[0].viewLeft, 6);
chai.assert.equal(filteredEvents[0].scale, 7);
chai.assert.equal(filteredEvents[0].oldScale, 8);
});
test('Merge ui events', function() {
let block1 = this.workspace.newBlock('field_variable_test_block', '1');
let block2 = this.workspace.newBlock('field_variable_test_block', '2');
let block3 = this.workspace.newBlock('field_variable_test_block', '3');
let events = [
new Blockly.Events.BubbleOpen(block1, true, 'comment'),
new Blockly.Events.Click(block1),
new Blockly.Events.BubbleOpen(block2, true, 'mutator'),
new Blockly.Events.Click(block2),
new Blockly.Events.BubbleOpen(block3, true, 'warning'),
new Blockly.Events.Click(block3)
];
let filteredEvents = eventUtils.filter(events, true);
// click event merged into corresponding *Open event
chai.assert.equal(filteredEvents.length, 3);
chai.assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BubbleOpen);
chai.assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BubbleOpen);
chai.assert.isTrue(filteredEvents[2] instanceof Blockly.Events.BubbleOpen);
chai.assert.equal(filteredEvents[0].bubbleType, 'comment');
chai.assert.equal(filteredEvents[1].bubbleType, 'mutator');
chai.assert.equal(filteredEvents[2].bubbleType, 'warning');
});
test('Colliding events not dropped', function() {
// Tests that events that collide on a (event, block, workspace) tuple
// but cannot be merged do not get dropped during filtering.
let block = this.workspace.newBlock('field_variable_test_block', '1');
let events = [
new Blockly.Events.Click(block),
new Blockly.Events.Ui(block, 'stackclick', undefined, undefined)
];
let filteredEvents = eventUtils.filter(events, true);
// click and stackclick should both exist
chai.assert.equal(filteredEvents.length, 2);
chai.assert.isTrue(filteredEvents[0] instanceof Blockly.Events.Click);
chai.assert.equal(filteredEvents[1].element, 'stackclick');
});
test('Merging null operations dropped', function() {
// Mutator composition could result in move events for blocks
// connected to the mutated block that were null operations. This
// leads to events in the undo/redo queue that do nothing, requiring
// an extra undo/redo to proceed to the next event. This test ensures
// that two move events that do get merged (disconnecting and
// reconnecting a block in response to a mutator change) are filtered
// from the queue.
let block = this.workspace.newBlock('field_variable_test_block', '1');
block.setParent(null);
let events = [];
addMoveEventParent(events, block, null);
addMoveEventParent(events, block, null);
let filteredEvents = eventUtils.filter(events, true);
// The two events should be merged, but because nothing has changed
// they will be filtered out.
chai.assert.equal(filteredEvents.length, 0);
});
test('Move events different blocks not merged', function() {
// Move events should only merge if they refer to the same block and are
// consecutive.
// See github.com/google/blockly/pull/1892 for a worked example showing
// how merging non-consecutive events can fail when replacing a shadow
// block.
let block1 = createSimpleTestBlock(this.workspace);
let block2 = createSimpleTestBlock(this.workspace);
let events = [];
addMoveEvent(events, block1, 1, 1);
addMoveEvent(events, block2, 1, 1);
events.push(new Blockly.Events.BlockDelete(block2));
addMoveEvent(events, block1, 2, 2);
let filteredEvents = eventUtils.filter(events, true);
// Nothing should have merged.
chai.assert.equal(filteredEvents.length, 4);
// test that the order hasn't changed
chai.assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockMove);
chai.assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove);
chai.assert.isTrue(filteredEvents[2] instanceof Blockly.Events.BlockDelete);
chai.assert.isTrue(filteredEvents[3] instanceof Blockly.Events.BlockMove);
});
});
suite('Firing', function() {
setup(function() {
this.changeListenerSpy = createFireChangeListenerSpy(this.workspace);
});
test('Block dispose triggers Delete', function() {
let workspaceSvg;
try {
let toolbox = document.getElementById('toolbox-categories');
workspaceSvg = Blockly.inject('blocklyDiv', {toolbox: toolbox});
let TEST_BLOCK_ID = 'test_block_id';
let genUidStub = createGenUidStubWithReturns(
[TEST_BLOCK_ID, 'test_group_id']);
let block = workspaceSvg.newBlock('');
block.initSvg();
block.setCommentText('test comment');
let expectedOldXml = Blockly.Xml.blockToDomWithXY(block);
let expectedId = block.id;
// Run all queued events.
this.clock.runAll();
this.eventsFireSpy.resetHistory();
let changeListenerSpy = createFireChangeListenerSpy(workspaceSvg);
block.dispose();
// Run all queued events.
this.clock.runAll();
// Expect two calls to genUid: one to set the block's ID, and one for
// the event group's ID for creating block.
sinon.assert.calledTwice(genUidStub);
assertNthCallEventArgEquals(
this.eventsFireSpy, 0, Blockly.Events.BlockDelete,
{oldXml: expectedOldXml, group: ''},
workspaceSvg.id, expectedId);
assertNthCallEventArgEquals(
changeListenerSpy, 0, Blockly.Events.BlockDelete,
{oldXml: expectedOldXml, group: ''},
workspaceSvg.id, expectedId);
// Expect the workspace to not have a variable with ID 'test_block_id'.
chai.assert.isNull(this.workspace.getVariableById(TEST_BLOCK_ID));
} finally {
workspaceTeardown.call(this, workspaceSvg);
}
});
test('New block new var', function() {
let TEST_BLOCK_ID = 'test_block_id';
let TEST_GROUP_ID = 'test_group_id';
let TEST_VAR_ID = 'test_var_id';
let genUidStub = createGenUidStubWithReturns(
[TEST_BLOCK_ID, TEST_GROUP_ID, TEST_VAR_ID]);
let _ = this.workspace.newBlock('field_variable_test_block');
let TEST_VAR_NAME = 'item'; // As defined in block's json.
// Run all queued events.
this.clock.runAll();
// Expect three calls to genUid: one to set the block's ID, one for the event
// group's ID, and one for the variable's ID.
sinon.assert.calledThrice(genUidStub);
// Expect two events fired: varCreate and block create.
sinon.assert.calledTwice(this.eventsFireSpy);
// Expect both events to trigger change listener.
sinon.assert.calledTwice(this.changeListenerSpy);
// Both events should be on undo stack
chai.assert.equal(this.workspace.undoStack_.length, 2,
'Undo stack length');
assertNthCallEventArgEquals(
this.changeListenerSpy, 0, Blockly.Events.VarCreate,
{group: TEST_GROUP_ID, varId: TEST_VAR_ID, varName: TEST_VAR_NAME},
this.workspace.id, undefined);
assertNthCallEventArgEquals(
this.changeListenerSpy, 1, Blockly.Events.BlockCreate,
{group: TEST_GROUP_ID}, this.workspace.id, TEST_BLOCK_ID);
// Expect the workspace to have a variable with ID 'test_var_id'.
chai.assert.isNotNull(this.workspace.getVariableById(TEST_VAR_ID));
});
test('New block new var xml', function() {
let TEST_GROUP_ID = 'test_group_id';
let genUidStub = createGenUidStubWithReturns(TEST_GROUP_ID);
let dom = Blockly.Xml.textToDom(
'' +
' ' +
' name1' +
' ' +
'');
Blockly.Xml.domToWorkspace(dom, this.workspace);
let TEST_BLOCK_ID = 'test_block_id';
let TEST_VAR_ID = 'test_var_id';
let TEST_VAR_NAME = 'name1';
// Run all queued events.
this.clock.runAll();
// Expect one call to genUid: for the event group's id
sinon.assert.calledOnce(genUidStub);
// When block is created using domToWorkspace, 5 events are fired:
// 1. varCreate (events disabled)
// 2. varCreate
// 3. block create
// 4. move (no-op, is filtered out)
// 5. finished loading
sinon.assert.callCount(this.eventsFireSpy, 5);
// The first varCreate and move event should have been ignored.
sinon.assert.callCount(this.changeListenerSpy, 3);
// Expect two events on undo stack: varCreate and block create.
chai.assert.equal(this.workspace.undoStack_.length, 2,
'Undo stack length');
assertNthCallEventArgEquals(
this.changeListenerSpy, 0, Blockly.Events.VarCreate,
{group: TEST_GROUP_ID, varId: TEST_VAR_ID, varName: TEST_VAR_NAME},
this.workspace.id, undefined);
assertNthCallEventArgEquals(
this.changeListenerSpy, 1, Blockly.Events.BlockCreate,
{group: TEST_GROUP_ID}, this.workspace.id, TEST_BLOCK_ID);
// Finished loading event should not be part of event group.
assertNthCallEventArgEquals(
this.changeListenerSpy, 2, Blockly.Events.FinishedLoading,
{group: ''}, this.workspace.id, undefined);
// Expect the workspace to have a variable with ID 'test_var_id'.
chai.assert.isNotNull(this.workspace.getVariableById(TEST_VAR_ID));
});
});
suite('Disable orphans', function() {
setup(function() {
// disableOrphans needs a WorkspaceSVG
let toolbox = document.getElementById('toolbox-categories');
this.workspace = Blockly.inject('blocklyDiv', {toolbox: toolbox});
});
teardown(function() {
workspaceTeardown.call(this, this.workspace);
});
test('Created orphan block is disabled', function() {
this.workspace.addChangeListener(eventUtils.disableOrphans);
let block = this.workspace.newBlock('controls_for');
block.initSvg();
block.render();
// Fire all events
this.clock.runAll();
chai.assert.isFalse(block.isEnabled(),
'Expected orphan block to be disabled after creation');
});
test('Created procedure block is enabled', function() {
this.workspace.addChangeListener(eventUtils.disableOrphans);
// Procedure block is never an orphan
let functionBlock = this.workspace.newBlock('procedures_defnoreturn');
functionBlock.initSvg();
functionBlock.render();
// Fire all events
this.clock.runAll();
chai.assert.isTrue(functionBlock.isEnabled(),
'Expected top-level procedure block to be enabled');
});
test('Moving a block to top-level disables it', function() {
this.workspace.addChangeListener(eventUtils.disableOrphans);
let functionBlock = this.workspace.newBlock('procedures_defnoreturn');
functionBlock.initSvg();
functionBlock.render();
let block = this.workspace.newBlock('controls_for');
block.initSvg();
block.render();
// Connect the block to the function block input stack
functionBlock.inputList[1].connection.connect(block.previousConnection);
// Disconnect it again
block.unplug(false);
// Fire all events
this.clock.runAll();
chai.assert.isFalse(block.isEnabled(),
'Expected disconnected block to be disabled');
});
test('Giving block a parent enables it', function() {
this.workspace.addChangeListener(eventUtils.disableOrphans);
let functionBlock = this.workspace.newBlock('procedures_defnoreturn');
functionBlock.initSvg();
functionBlock.render();
let block = this.workspace.newBlock('controls_for');
block.initSvg();
block.render();
// Connect the block to the function block input stack
functionBlock.inputList[1].connection.connect(block.previousConnection);
// Fire all events
this.clock.runAll();
chai.assert.isTrue(block.isEnabled(),
'Expected block to be enabled after connecting to parent');
});
test('disableOrphans events are not undoable', function() {
this.workspace.addChangeListener(eventUtils.disableOrphans);
let functionBlock = this.workspace.newBlock('procedures_defnoreturn');
functionBlock.initSvg();
functionBlock.render();
let block = this.workspace.newBlock('controls_for');
block.initSvg();
block.render();
// Connect the block to the function block input stack
functionBlock.inputList[1].connection.connect(block.previousConnection);
// Disconnect it again
block.unplug(false);
// Fire all events
this.clock.runAll();
let disabledEvents = this.workspace.getUndoStack().filter(function(e) {
return e.element === 'disabled';
});
chai.assert.isEmpty(disabledEvents,
'Undo stack should not contain any disabled events');
});
});
});