mirror of
https://github.com/google/blockly.git
synced 2026-01-04 15:40:08 +01:00
* feat: Invalid Blocks * Rename the new json property from invalid to invalidReasons. * Merged isValid into isEnabled. * Minor fixes. * More minor fixes. * Reverting some stuff that didn't need to change. * Addressing PR feedback. * Update the BlockInfo interface to match State. * Make BlockChange.disabledReason private.
369 lines
12 KiB
JavaScript
369 lines
12 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2023 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {
|
|
sharedTestSetup,
|
|
sharedTestTeardown,
|
|
} from './test_helpers/setup_teardown.js';
|
|
import {defineEmptyBlock} from './test_helpers/block_definitions.js';
|
|
import {MockIcon, MockSerializableIcon} from './test_helpers/icon_mocks.js';
|
|
|
|
suite('Icon', function () {
|
|
setup(function () {
|
|
this.clock = sharedTestSetup.call(this, {fireEventsNow: false}).clock;
|
|
defineEmptyBlock();
|
|
});
|
|
|
|
teardown(function () {
|
|
sharedTestTeardown.call(this);
|
|
});
|
|
|
|
class MockNonSerializableIcon extends MockIcon {
|
|
getType() {
|
|
return 'non-serializable icon';
|
|
}
|
|
}
|
|
|
|
function createHeadlessWorkspace() {
|
|
return new Blockly.Workspace();
|
|
}
|
|
|
|
function createWorkspaceSvg() {
|
|
const workspace = new Blockly.WorkspaceSvg(new Blockly.Options({}));
|
|
workspace.createDom();
|
|
return workspace;
|
|
}
|
|
|
|
function createUninitializedBlock(workspace) {
|
|
return workspace.newBlock('empty_block');
|
|
}
|
|
|
|
function createInitializedBlock(workspace) {
|
|
const block = workspace.newBlock('empty_block');
|
|
block.initSvg();
|
|
block.render();
|
|
return block;
|
|
}
|
|
|
|
function createHeadlessBlock(workspace) {
|
|
return createUninitializedBlock(workspace);
|
|
}
|
|
|
|
suite('Hooks getting properly triggered by the block', function () {
|
|
suite('Triggering view initialization', function () {
|
|
test('initView is not called by headless blocks', function () {
|
|
const workspace = createHeadlessWorkspace();
|
|
const block = createUninitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const initViewSpy = sinon.spy(icon, 'initView');
|
|
|
|
block.addIcon(icon);
|
|
|
|
chai.assert.isFalse(
|
|
initViewSpy.called,
|
|
'Expected initView to not be called',
|
|
);
|
|
});
|
|
|
|
test(
|
|
'initView is called by headful blocks that are currently ' +
|
|
'rendered when the icon is added',
|
|
function () {
|
|
const workspace = new Blockly.WorkspaceSvg(new Blockly.Options({}));
|
|
workspace.createDom();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const initViewSpy = sinon.spy(icon, 'initView');
|
|
|
|
block.addIcon(icon);
|
|
chai.assert.isTrue(
|
|
initViewSpy.calledOnce,
|
|
'Expected initView to be called',
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
suite('Triggering applying colors', function () {
|
|
test('applyColour is not called by headless blocks', function () {
|
|
const workspace = createHeadlessWorkspace();
|
|
const block = createUninitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const applyColourSpy = sinon.spy(icon, 'applyColour');
|
|
|
|
block.addIcon(icon);
|
|
|
|
chai.assert.isFalse(
|
|
applyColourSpy.called,
|
|
'Expected applyColour to not be called',
|
|
);
|
|
});
|
|
|
|
test(
|
|
'applyColour is called by headful blocks that are currently ' +
|
|
'rendered when the icon is added',
|
|
function () {
|
|
const workspace = createWorkspaceSvg();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const applyColourSpy = sinon.spy(icon, 'applyColour');
|
|
|
|
block.addIcon(icon);
|
|
chai.assert.isTrue(
|
|
applyColourSpy.calledOnce,
|
|
'Expected applyColour to be called',
|
|
);
|
|
},
|
|
);
|
|
|
|
test("applyColour is called when the block's color changes", function () {
|
|
const workspace = createWorkspaceSvg();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const applyColourSpy = sinon.spy(icon, 'applyColour');
|
|
|
|
block.addIcon(icon);
|
|
applyColourSpy.resetHistory();
|
|
block.setColour('#cccccc');
|
|
chai.assert.isTrue(
|
|
applyColourSpy.calledOnce,
|
|
'Expected applyColour to be called',
|
|
);
|
|
});
|
|
|
|
test("applyColour is called when the block's style changes", function () {
|
|
const workspace = createWorkspaceSvg();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const applyColourSpy = sinon.spy(icon, 'applyColour');
|
|
|
|
block.addIcon(icon);
|
|
applyColourSpy.resetHistory();
|
|
block.setStyle('logic_block');
|
|
chai.assert.isTrue(
|
|
applyColourSpy.calledOnce,
|
|
'Expected applyColour to be called',
|
|
);
|
|
});
|
|
|
|
test('applyColour is called when the block is disabled', function () {
|
|
const workspace = createWorkspaceSvg();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const applyColourSpy = sinon.spy(icon, 'applyColour');
|
|
|
|
block.addIcon(icon);
|
|
applyColourSpy.resetHistory();
|
|
block.setDisabledReason(true, 'test reason');
|
|
chai.assert.isTrue(
|
|
applyColourSpy.calledOnce,
|
|
'Expected applyColour to be called',
|
|
);
|
|
});
|
|
|
|
test('applyColour is called when the block becomes a shadow', function () {
|
|
const workspace = createWorkspaceSvg();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const applyColourSpy = sinon.spy(icon, 'applyColour');
|
|
|
|
block.addIcon(icon);
|
|
applyColourSpy.resetHistory();
|
|
block.setShadow(true);
|
|
chai.assert.isTrue(
|
|
applyColourSpy.calledOnce,
|
|
'Expected applyColour to be called',
|
|
);
|
|
});
|
|
});
|
|
|
|
suite('Triggering updating editability', function () {
|
|
test('updateEditable is not called by headless blocks', function () {
|
|
const workspace = createHeadlessWorkspace();
|
|
const block = createUninitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const updateEditableSpy = sinon.spy(icon, 'updateEditable');
|
|
|
|
block.addIcon(icon);
|
|
|
|
chai.assert.isFalse(
|
|
updateEditableSpy.called,
|
|
'Expected updateEditable to not be called',
|
|
);
|
|
});
|
|
|
|
test(
|
|
'updateEditable is called by headful blocks that are currently ' +
|
|
'rendered when the icon is added',
|
|
function () {
|
|
const workspace = createWorkspaceSvg();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const updateEditableSpy = sinon.spy(icon, 'updateEditable');
|
|
|
|
block.addIcon(icon);
|
|
chai.assert.isTrue(
|
|
updateEditableSpy.calledOnce,
|
|
'Expected updateEditable to be called',
|
|
);
|
|
},
|
|
);
|
|
|
|
test('updateEditable is called when the block is made ineditable', function () {
|
|
const workspace = createWorkspaceSvg();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const updateEditableSpy = sinon.spy(icon, 'updateEditable');
|
|
|
|
block.addIcon(icon);
|
|
updateEditableSpy.resetHistory();
|
|
block.setEditable(false);
|
|
chai.assert.isTrue(
|
|
updateEditableSpy.calledOnce,
|
|
'Expected updateEditable to be called',
|
|
);
|
|
});
|
|
|
|
test('updateEditable is called when the block is made editable', function () {
|
|
const workspace = createWorkspaceSvg();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const updateEditableSpy = sinon.spy(icon, 'updateEditable');
|
|
|
|
block.addIcon(icon);
|
|
block.setEditable(false);
|
|
updateEditableSpy.resetHistory();
|
|
block.setEditable(true);
|
|
chai.assert.isTrue(
|
|
updateEditableSpy.calledOnce,
|
|
'Expected updateEditable to be called',
|
|
);
|
|
});
|
|
});
|
|
|
|
suite('Triggering updating collapsed-ness', function () {
|
|
test('updateCollapsed is not called by headless blocks', function () {
|
|
const workspace = createHeadlessWorkspace();
|
|
const block = createUninitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const updateCollapsedSpy = sinon.spy(icon, 'updateCollapsed');
|
|
|
|
block.addIcon(icon);
|
|
block.setCollapsed(true);
|
|
block.setCollapsed(false);
|
|
|
|
chai.assert.isFalse(
|
|
updateCollapsedSpy.called,
|
|
'Expected updateCollapsed to not be called',
|
|
);
|
|
});
|
|
|
|
test('updateCollapsed is called when the block is collapsed', function () {
|
|
const workspace = createWorkspaceSvg();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const updateCollapsedSpy = sinon.stub(icon, 'updateCollapsed');
|
|
block.addIcon(icon);
|
|
|
|
updateCollapsedSpy.resetHistory();
|
|
block.setCollapsed(true);
|
|
this.clock.runAll();
|
|
|
|
chai.assert.isTrue(
|
|
updateCollapsedSpy.called,
|
|
'Expected updateCollapsed to be called',
|
|
);
|
|
});
|
|
|
|
test('updateCollapsed is called when the block is expanded', function () {
|
|
const workspace = createWorkspaceSvg();
|
|
const block = createInitializedBlock(workspace);
|
|
const icon = new MockIcon();
|
|
const updateCollapsedSpy = sinon.spy(icon, 'updateCollapsed');
|
|
block.addIcon(icon);
|
|
|
|
block.setCollapsed(true);
|
|
block.setCollapsed(false);
|
|
this.clock.runAll();
|
|
|
|
chai.assert.isTrue(
|
|
updateCollapsedSpy.called,
|
|
'Expected updateCollapsed to be called',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
suite('Serialization', function () {
|
|
test('serializable icons are saved', function () {
|
|
const block = createHeadlessBlock(createHeadlessWorkspace());
|
|
block.addIcon(new MockSerializableIcon());
|
|
const json = Blockly.serialization.blocks.save(block);
|
|
chai.assert.deepNestedInclude(
|
|
json,
|
|
{'icons': {'serializable icon': 'some state'}},
|
|
'Expected the JSON to include the saved state of the ' +
|
|
'serializable icon.',
|
|
);
|
|
});
|
|
|
|
test('non-serializable icons are not saved', function () {
|
|
const block = createHeadlessBlock(createHeadlessWorkspace());
|
|
block.addIcon(new MockNonSerializableIcon());
|
|
const json = Blockly.serialization.blocks.save(block);
|
|
chai.assert.notProperty(
|
|
json,
|
|
'icons',
|
|
'Expected the JSON to not include any saved state for icons',
|
|
);
|
|
});
|
|
});
|
|
|
|
suite('Deserialization', function () {
|
|
test('registered icons are instantiated and added to the block', function () {
|
|
Blockly.icons.registry.register(
|
|
'serializable icon',
|
|
MockSerializableIcon,
|
|
);
|
|
|
|
const workspace = createHeadlessWorkspace();
|
|
const json = {
|
|
'type': 'empty_block',
|
|
'icons': {
|
|
'serializable icon': 'some state',
|
|
},
|
|
};
|
|
const block = Blockly.serialization.blocks.append(json, workspace);
|
|
chai.assert.equal(
|
|
block.getIcon('serializable icon').state,
|
|
'some state',
|
|
'Expected the icon to have been properly instantiated and ' +
|
|
'deserialized',
|
|
);
|
|
|
|
Blockly.icons.registry.unregister('serializable icon');
|
|
});
|
|
|
|
test('trying to deserialize an unregistered icon throws an error', function () {
|
|
const workspace = createHeadlessWorkspace();
|
|
const json = {
|
|
'type': 'empty_block',
|
|
'icons': {
|
|
'serializable icon': 'some state',
|
|
},
|
|
};
|
|
chai.assert.throws(
|
|
() => {
|
|
Blockly.serialization.blocks.append(json, workspace);
|
|
},
|
|
Blockly.serialization.exceptions.UnregisteredIcon,
|
|
'',
|
|
'Expected deserializing an unregistered icon to throw',
|
|
);
|
|
});
|
|
});
|
|
});
|