Files
blockly/tests/mocha/icon_test.js
John Nesky cee7f916bb feat!: Invalid Blocks (#7958)
* 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.
2024-04-17 19:47:51 -07:00

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',
);
});
});
});