Merge branch 'google:develop' into fix-browser-tests-2025-06

This commit is contained in:
RoboErikG
2025-07-07 10:57:42 -07:00
committed by GitHub
43 changed files with 1667 additions and 1371 deletions

View File

@@ -29,7 +29,10 @@ suite('Connection checker', function () {
}
test('Target Null', function () {
const connection = new Blockly.Connection({}, ConnectionType.INPUT_VALUE);
const connection = new Blockly.Connection(
{id: 'test'},
ConnectionType.INPUT_VALUE,
);
assertReasonHelper(
this.checker,
connection,
@@ -38,7 +41,7 @@ suite('Connection checker', function () {
);
});
test('Target Self', function () {
const block = {workspace: 1};
const block = {id: 'test', workspace: 1};
const connection1 = new Blockly.Connection(
block,
ConnectionType.INPUT_VALUE,
@@ -57,11 +60,11 @@ suite('Connection checker', function () {
});
test('Different Workspaces', function () {
const connection1 = new Blockly.Connection(
{workspace: 1},
{id: 'test1', workspace: 1},
ConnectionType.INPUT_VALUE,
);
const connection2 = new Blockly.Connection(
{workspace: 2},
{id: 'test2', workspace: 2},
ConnectionType.OUTPUT_VALUE,
);
@@ -76,10 +79,10 @@ suite('Connection checker', function () {
setup(function () {
// We have to declare each separately so that the connections belong
// on different blocks.
const prevBlock = {isShadow: function () {}};
const nextBlock = {isShadow: function () {}};
const outBlock = {isShadow: function () {}};
const inBlock = {isShadow: function () {}};
const prevBlock = {id: 'test1', isShadow: function () {}};
const nextBlock = {id: 'test2', isShadow: function () {}};
const outBlock = {id: 'test3', isShadow: function () {}};
const inBlock = {id: 'test4', isShadow: function () {}};
this.previous = new Blockly.Connection(
prevBlock,
ConnectionType.PREVIOUS_STATEMENT,
@@ -197,11 +200,13 @@ suite('Connection checker', function () {
suite('Shadows', function () {
test('Previous Shadow', function () {
const prevBlock = {
id: 'test1',
isShadow: function () {
return true;
},
};
const nextBlock = {
id: 'test2',
isShadow: function () {
return false;
},
@@ -224,11 +229,13 @@ suite('Connection checker', function () {
});
test('Next Shadow', function () {
const prevBlock = {
id: 'test1',
isShadow: function () {
return false;
},
};
const nextBlock = {
id: 'test2',
isShadow: function () {
return true;
},
@@ -251,11 +258,13 @@ suite('Connection checker', function () {
});
test('Prev and Next Shadow', function () {
const prevBlock = {
id: 'test1',
isShadow: function () {
return true;
},
};
const nextBlock = {
id: 'test2',
isShadow: function () {
return true;
},
@@ -278,11 +287,13 @@ suite('Connection checker', function () {
});
test('Output Shadow', function () {
const outBlock = {
id: 'test1',
isShadow: function () {
return true;
},
};
const inBlock = {
id: 'test2',
isShadow: function () {
return false;
},
@@ -305,11 +316,13 @@ suite('Connection checker', function () {
});
test('Input Shadow', function () {
const outBlock = {
id: 'test1',
isShadow: function () {
return false;
},
};
const inBlock = {
id: 'test2',
isShadow: function () {
return true;
},
@@ -332,11 +345,13 @@ suite('Connection checker', function () {
});
test('Output and Input Shadow', function () {
const outBlock = {
id: 'test1',
isShadow: function () {
return true;
},
};
const inBlock = {
id: 'test2',
isShadow: function () {
return true;
},
@@ -373,9 +388,11 @@ suite('Connection checker', function () {
};
test('Output connected, adding previous', function () {
const outBlock = {
id: 'test1',
isShadow: function () {},
};
const inBlock = {
id: 'test2',
isShadow: function () {},
};
const outCon = new Blockly.Connection(
@@ -394,6 +411,7 @@ suite('Connection checker', function () {
ConnectionType.PREVIOUS_STATEMENT,
);
const nextBlock = {
id: 'test3',
isShadow: function () {},
};
const nextCon = new Blockly.Connection(
@@ -410,9 +428,11 @@ suite('Connection checker', function () {
});
test('Previous connected, adding output', function () {
const prevBlock = {
id: 'test1',
isShadow: function () {},
};
const nextBlock = {
id: 'test2',
isShadow: function () {},
};
const prevCon = new Blockly.Connection(
@@ -431,6 +451,7 @@ suite('Connection checker', function () {
ConnectionType.OUTPUT_VALUE,
);
const inBlock = {
id: 'test3',
isShadow: function () {},
};
const inCon = new Blockly.Connection(
@@ -449,8 +470,14 @@ suite('Connection checker', function () {
});
suite('Check Types', function () {
setup(function () {
this.con1 = new Blockly.Connection({}, ConnectionType.PREVIOUS_STATEMENT);
this.con2 = new Blockly.Connection({}, ConnectionType.NEXT_STATEMENT);
this.con1 = new Blockly.Connection(
{id: 'test1'},
ConnectionType.PREVIOUS_STATEMENT,
);
this.con2 = new Blockly.Connection(
{id: 'test2'},
ConnectionType.NEXT_STATEMENT,
);
});
function assertCheckTypes(checker, one, two) {
assert.isTrue(checker.doTypeChecks(one, two));

View File

@@ -5,6 +5,7 @@
*/
import {ConnectionType} from '../../build/src/core/connection_type.js';
import * as idGenerator from '../../build/src/core/utils/idgenerator.js';
import {assert} from '../../node_modules/chai/chai.js';
import {
sharedTestSetup,
@@ -31,7 +32,7 @@ suite('Connection Database', function () {
};
workspace.connectionDBList[type] = opt_database || this.database;
const connection = new Blockly.RenderedConnection(
{workspace: workspace},
{id: idGenerator.getNextUniqueId(), workspace: workspace},
type,
);
connection.x = x;

View File

@@ -355,6 +355,7 @@ suite('Events', function () {
suite('With variable getter blocks', function () {
setup(function () {
this.TEST_BLOCK_ID = 'test_block_id';
this.genUidStub = createGenUidStubWithReturns([
this.TEST_BLOCK_ID,
'test_var_id',

View File

@@ -294,4 +294,300 @@ suite('Text Input Fields', function () {
this.assertValue('test text');
});
});
suite('Use editor', function () {
setup(function () {
this.blockJson = {
'type': 'math_arithmetic',
'id': 'test_arithmetic_block',
'fields': {
'OP': 'ADD',
},
'inputs': {
'A': {
'shadow': {
'type': 'math_number',
'id': 'left_input_block',
'name': 'test_name',
'fields': {
'NUM': 1,
},
},
},
'B': {
'shadow': {
'type': 'math_number',
'id': 'right_input_block',
'fields': {
'NUM': 2,
},
},
},
},
};
this.getFieldFromShadowBlock = function (shadowBlock) {
return shadowBlock.getFields().next().value;
};
this.simulateTypingIntoInput = (inputElem, newText) => {
// Typing into an input field changes its value directly and then fires
// an InputEvent (which FieldInput relies on to automatically
// synchronize its state).
inputElem.value = newText;
inputElem.dispatchEvent(new InputEvent('input'));
};
});
// The block being tested doesn't use full-block fields in Geras.
suite('Geras theme', function () {
setup(function () {
this.workspace = Blockly.inject('blocklyDiv', {
renderer: 'geras',
});
Blockly.serialization.blocks.append(this.blockJson, this.workspace);
// The workspace actually needs to be visible for focus.
document.getElementById('blocklyDiv').style.visibility = 'visible';
});
teardown(function () {
document.getElementById('blocklyDiv').style.visibility = 'hidden';
workspaceTeardown.call(this, this.workspace);
});
test('No editor open by default', function () {
// The editor is only opened if its indicated that it should be open.
assert.isNull(document.querySelector('.blocklyHtmlInput'));
});
test('Type in editor with escape does not change field value', async function () {
const block = this.workspace.getBlockById('left_input_block');
const field = this.getFieldFromShadowBlock(block);
field.showEditor();
// This must be called to avoid editor resize logic throwing an error.
await Blockly.renderManagement.finishQueuedRenders();
// Change the value of the field's input through its editor.
const fieldEditor = document.querySelector('.blocklyHtmlInput');
this.simulateTypingIntoInput(fieldEditor, 'updated value');
fieldEditor.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Escape',
}),
);
// 'Escape' will avoid saving the edited field value and close the editor.
assert.equal(field.getValue(), 1);
assert.isNull(document.querySelector('.blocklyHtmlInput'));
});
test('Type in editor with enter changes field value', async function () {
const block = this.workspace.getBlockById('left_input_block');
const field = this.getFieldFromShadowBlock(block);
field.showEditor();
// This must be called to avoid editor resize logic throwing an error.
await Blockly.renderManagement.finishQueuedRenders();
// Change the value of the field's input through its editor.
const fieldEditor = document.querySelector('.blocklyHtmlInput');
this.simulateTypingIntoInput(fieldEditor, '10');
fieldEditor.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Enter',
}),
);
// 'Enter' will save the edited result and close the editor.
assert.equal(field.getValue(), 10);
assert.isNull(document.querySelector('.blocklyHtmlInput'));
});
test('Not finishing editing does not return ephemeral focus', async function () {
const block = this.workspace.getBlockById('left_input_block');
const field = this.getFieldFromShadowBlock(block);
Blockly.getFocusManager().focusNode(field);
field.showEditor();
// This must be called to avoid editor resize logic throwing an error.
await Blockly.renderManagement.finishQueuedRenders();
// Change the value of the field's input through its editor.
const fieldEditor = document.querySelector('.blocklyHtmlInput');
this.simulateTypingIntoInput(fieldEditor, '10');
// If the editor doesn't restore focus then the current focused element is
// still the editor.
assert.strictEqual(document.activeElement, fieldEditor);
});
test('Finishing editing returns ephemeral focus', async function () {
const block = this.workspace.getBlockById('left_input_block');
const field = this.getFieldFromShadowBlock(block);
Blockly.getFocusManager().focusNode(field);
field.showEditor();
// This must be called to avoid editor resize logic throwing an error.
await Blockly.renderManagement.finishQueuedRenders();
// Change the value of the field's input through its editor.
const fieldEditor = document.querySelector('.blocklyHtmlInput');
this.simulateTypingIntoInput(fieldEditor, '10');
fieldEditor.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Escape',
}),
);
// Verify that exiting the editor restores focus back to the field.
assert.strictEqual(Blockly.getFocusManager().getFocusedNode(), field);
assert.strictEqual(document.activeElement, field.getFocusableElement());
});
test('Opening an editor, tabbing, then editing changes the second field', async function () {
const leftInputBlock = this.workspace.getBlockById('left_input_block');
const rightInputBlock =
this.workspace.getBlockById('right_input_block');
const leftField = this.getFieldFromShadowBlock(leftInputBlock);
const rightField = this.getFieldFromShadowBlock(rightInputBlock);
leftField.showEditor();
// This must be called to avoid editor resize logic throwing an error.
await Blockly.renderManagement.finishQueuedRenders();
// Tab, then edit and close the editor.
document.querySelector('.blocklyHtmlInput').dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Tab',
}),
);
const rightFieldEditor = document.querySelector('.blocklyHtmlInput');
this.simulateTypingIntoInput(rightFieldEditor, '15');
rightFieldEditor.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Enter',
}),
);
// Verify that only the right field changed (due to the tab).
assert.equal(leftField.getValue(), 1);
assert.equal(rightField.getValue(), 15);
assert.isNull(document.querySelector('.blocklyHtmlInput'));
});
test('Opening an editor, tabbing, then editing changes focus to the second field', async function () {
const leftInputBlock = this.workspace.getBlockById('left_input_block');
const rightInputBlock =
this.workspace.getBlockById('right_input_block');
const leftField = this.getFieldFromShadowBlock(leftInputBlock);
const rightField = this.getFieldFromShadowBlock(rightInputBlock);
Blockly.getFocusManager().focusNode(leftField);
leftField.showEditor();
// This must be called to avoid editor resize logic throwing an error.
await Blockly.renderManagement.finishQueuedRenders();
// Tab, then edit and close the editor.
document.querySelector('.blocklyHtmlInput').dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Tab',
}),
);
const rightFieldEditor = document.querySelector('.blocklyHtmlInput');
this.simulateTypingIntoInput(rightFieldEditor, '15');
rightFieldEditor.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Enter',
}),
);
// Verify that the tab causes focus to change to the right field.
assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
rightField,
);
assert.strictEqual(
document.activeElement,
rightField.getFocusableElement(),
);
});
});
// The block being tested uses full-block fields in Zelos.
suite('Zelos theme', function () {
setup(function () {
this.workspace = Blockly.inject('blocklyDiv', {
renderer: 'zelos',
});
Blockly.serialization.blocks.append(this.blockJson, this.workspace);
// The workspace actually needs to be visible for focus.
document.getElementById('blocklyDiv').style.visibility = 'visible';
});
teardown(function () {
document.getElementById('blocklyDiv').style.visibility = 'hidden';
workspaceTeardown.call(this, this.workspace);
});
test('Opening an editor, tabbing, then editing full block field changes the second field', async function () {
const leftInputBlock = this.workspace.getBlockById('left_input_block');
const rightInputBlock =
this.workspace.getBlockById('right_input_block');
const leftField = this.getFieldFromShadowBlock(leftInputBlock);
const rightField = this.getFieldFromShadowBlock(rightInputBlock);
leftField.showEditor();
// This must be called to avoid editor resize logic throwing an error.
await Blockly.renderManagement.finishQueuedRenders();
// Tab, then edit and close the editor.
document.querySelector('.blocklyHtmlInput').dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Tab',
}),
);
const rightFieldEditor = document.querySelector('.blocklyHtmlInput');
this.simulateTypingIntoInput(rightFieldEditor, '15');
rightFieldEditor.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Enter',
}),
);
// Verify that only the right field changed (due to the tab).
assert.equal(leftField.getValue(), 1);
assert.equal(rightField.getValue(), 15);
assert.isNull(document.querySelector('.blocklyHtmlInput'));
});
test('Opening an editor, tabbing, then editing full block field changes focus to the second field', async function () {
const leftInputBlock = this.workspace.getBlockById('left_input_block');
const rightInputBlock =
this.workspace.getBlockById('right_input_block');
const leftField = this.getFieldFromShadowBlock(leftInputBlock);
Blockly.getFocusManager().focusNode(leftInputBlock);
leftField.showEditor();
// This must be called to avoid editor resize logic throwing an error.
await Blockly.renderManagement.finishQueuedRenders();
// Tab, then edit and close the editor.
document.querySelector('.blocklyHtmlInput').dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Tab',
}),
);
const rightFieldEditor = document.querySelector('.blocklyHtmlInput');
this.simulateTypingIntoInput(rightFieldEditor, '15');
rightFieldEditor.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Enter',
}),
);
// Verify that the tab causes focus to change to the right field block.
assert.strictEqual(
Blockly.getFocusManager().getFocusedNode(),
rightInputBlock,
);
assert.strictEqual(
document.activeElement,
rightInputBlock.getFocusableElement(),
);
});
});
});
});

View File

@@ -249,6 +249,54 @@ suite('FocusManager', function () {
// The second register should not fail since the tree was previously unregistered.
});
test('for tree with missing ID throws error', function () {
const rootNode = this.testFocusableTree1.getRootFocusableNode();
const rootElem = rootNode.getFocusableElement();
const oldId = rootElem.id;
rootElem.removeAttribute('id');
const errorMsgRegex =
/Attempting to register a tree with a root element that has an invalid ID.+?/;
assert.throws(
() => this.focusManager.registerTree(this.testFocusableTree1),
errorMsgRegex,
);
// Restore the ID for other tests.
rootElem.id = oldId;
});
test('for tree with null ID throws error', function () {
const rootNode = this.testFocusableTree1.getRootFocusableNode();
const rootElem = rootNode.getFocusableElement();
const oldId = rootElem.id;
rootElem.setAttribute('id', null);
const errorMsgRegex =
/Attempting to register a tree with a root element that has an invalid ID.+?/;
assert.throws(
() => this.focusManager.registerTree(this.testFocusableTree1),
errorMsgRegex,
);
// Restore the ID for other tests.
rootElem.id = oldId;
});
test('for tree with empty throws error', function () {
const rootNode = this.testFocusableTree1.getRootFocusableNode();
const rootElem = rootNode.getFocusableElement();
const oldId = rootElem.id;
rootElem.setAttribute('id', '');
const errorMsgRegex =
/Attempting to register a tree with a root element that has an invalid ID.+?/;
assert.throws(
() => this.focusManager.registerTree(this.testFocusableTree1),
errorMsgRegex,
);
// Restore the ID for other tests.
rootElem.id = oldId;
});
test('for unmanaged tree does not overwrite tab index', function () {
this.focusManager.registerTree(this.testFocusableTree1, false);

View File

@@ -348,6 +348,80 @@ suite('FocusableTreeTraverser', function () {
});
suite('findFocusableNodeFor()', function () {
test('for element without ID returns null', function () {
const tree = this.testFocusableTree1;
const rootNode = tree.getRootFocusableNode();
const rootElem = rootNode.getFocusableElement();
const oldId = rootElem.id;
// Normally it's not valid to miss an ID, but it can realistically happen.
rootElem.removeAttribute('id');
const finding = FocusableTreeTraverser.findFocusableNodeFor(
rootElem,
tree,
);
// Restore the ID for other tests.
rootElem.setAttribute('id', oldId);
assert.isNull(finding);
});
test('for element with null ID returns null', function () {
const tree = this.testFocusableTree1;
const rootNode = tree.getRootFocusableNode();
const rootElem = rootNode.getFocusableElement();
const oldId = rootElem.id;
// Normally it's not valid to miss an ID, but it can realistically happen.
rootElem.setAttribute('id', null);
const finding = FocusableTreeTraverser.findFocusableNodeFor(
rootElem,
tree,
);
// Restore the ID for other tests.
rootElem.setAttribute('id', oldId);
assert.isNull(finding);
});
test('for element with null ID string returns null', function () {
const tree = this.testFocusableTree1;
const rootNode = tree.getRootFocusableNode();
const rootElem = rootNode.getFocusableElement();
const oldId = rootElem.id;
// This is a quirky version of the null variety above that's actually
// functionallity equivalent (since 'null' is converted to a string).
rootElem.setAttribute('id', 'null');
const finding = FocusableTreeTraverser.findFocusableNodeFor(
rootElem,
tree,
);
// Restore the ID for other tests.
rootElem.setAttribute('id', oldId);
assert.isNull(finding);
});
test('for element with empty ID returns null', function () {
const tree = this.testFocusableTree1;
const rootNode = tree.getRootFocusableNode();
const rootElem = rootNode.getFocusableElement();
const oldId = rootElem.id;
// An empty ID is invalid since it will potentially conflict with other
// elements, and element IDs must be unique for focus management.
rootElem.setAttribute('id', '');
const finding = FocusableTreeTraverser.findFocusableNodeFor(
rootElem,
tree,
);
// Restore the ID for other tests.
rootElem.setAttribute('id', oldId);
assert.isNull(finding);
});
test('for root element returns root', function () {
const tree = this.testFocusableTree1;
const rootNode = tree.getRootFocusableNode();

View File

@@ -434,13 +434,13 @@ suite('Keyboard Shortcut Items', function () {
});
});
});
// Do not undo if a gesture is in progress.
suite('Gesture in progress', function () {
// Do not undo if a drag is in progress.
suite('Drag in progress', function () {
testCases.forEach(function (testCase) {
const testCaseName = testCase[0];
const keyEvent = testCase[1];
test(testCaseName, function () {
sinon.stub(Blockly.Gesture, 'inProgress').returns(true);
sinon.stub(this.workspace, 'isDragging').returns(true);
this.injectionDiv.dispatchEvent(keyEvent);
sinon.assert.notCalled(this.undoSpy);
sinon.assert.notCalled(this.hideChaffSpy);
@@ -494,13 +494,13 @@ suite('Keyboard Shortcut Items', function () {
});
});
});
// Do not undo if a gesture is in progress.
suite('Gesture in progress', function () {
// Do not redo if a drag is in progress.
suite('Drag in progress', function () {
testCases.forEach(function (testCase) {
const testCaseName = testCase[0];
const keyEvent = testCase[1];
test(testCaseName, function () {
sinon.stub(Blockly.Gesture, 'inProgress').returns(true);
sinon.stub(this.workspace, 'isDragging').returns(true);
this.injectionDiv.dispatchEvent(keyEvent);
sinon.assert.notCalled(this.redoSpy);
sinon.assert.notCalled(this.hideChaffSpy);
@@ -534,8 +534,8 @@ suite('Keyboard Shortcut Items', function () {
sinon.assert.calledWith(this.undoSpy, true);
sinon.assert.calledOnce(this.hideChaffSpy);
});
test('Not called when a gesture is in progress', function () {
sinon.stub(Blockly.Gesture, 'inProgress').returns(true);
test('Not called when a drag is in progress', function () {
sinon.stub(this.workspace, 'isDragging').returns(true);
this.injectionDiv.dispatchEvent(this.ctrlYEvent);
sinon.assert.notCalled(this.undoSpy);
sinon.assert.notCalled(this.hideChaffSpy);