From d5077f3d9586606c742e770bb1521e984984d4f5 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 1 May 2026 15:47:22 -0700 Subject: [PATCH 1/6] feat: Insert blocks at focus point --- .../core/dragging/block_drag_strategy.ts | 99 ++++++++++-- .../tests/mocha/keyboard_movement_test.js | 141 +++++++++++++++++- 2 files changed, 227 insertions(+), 13 deletions(-) diff --git a/packages/blockly/core/dragging/block_drag_strategy.ts b/packages/blockly/core/dragging/block_drag_strategy.ts index a84f4a3cccf..6c6c531eb6d 100644 --- a/packages/blockly/core/dragging/block_drag_strategy.ts +++ b/packages/blockly/core/dragging/block_drag_strategy.ts @@ -15,6 +15,7 @@ import {ConnectionType} from '../connection_type.js'; import type {BlockMove} from '../events/events_block_move.js'; import {EventType} from '../events/type.js'; import * as eventUtils from '../events/utils.js'; +import {FocusManager} from '../focus_manager.js'; import {showUnconstrainedMoveHint} from '../hints.js'; import type {IBubble} from '../interfaces/i_bubble.js'; import type {IConnectionPreviewer} from '../interfaces/i_connection_previewer.js'; @@ -26,7 +27,7 @@ import * as layers from '../layers.js'; import {Msg} from '../msg.js'; import * as registry from '../registry.js'; import {finishQueuedRenders} from '../render_management.js'; -import type {RenderedConnection} from '../rendered_connection.js'; +import {RenderedConnection} from '../rendered_connection.js'; import * as blocks from '../serialization/blocks.js'; import {Coordinate} from '../utils.js'; import * as aria from '../utils/aria.js'; @@ -243,10 +244,6 @@ export class BlockDragStrategy implements IDragStrategy { } this.block.setDragging(true); - this.workspace.getLayerManager()?.moveToDragLayer(this.block); - this.getVisibleBubbles(this.block).forEach((bubble) => { - this.workspace.getLayerManager()?.moveToDragLayer(bubble, false); - }); // For keyboard-driven moves, cache a list of valid connection points for // use in constrained moved mode. @@ -254,7 +251,6 @@ export class BlockDragStrategy implements IDragStrategy { this.cacheAllConnectionPairs(); // Scooch the block to be offset from the connection preview indicator. - this.block.moveDuringDrag(this.startLoc); const neighbour = this.updateConnectionPreview( this.block, new Coordinate(0, 0), @@ -262,10 +258,11 @@ export class BlockDragStrategy implements IDragStrategy { if (neighbour) { let offset: Coordinate; if (neighbour.type === ConnectionType.PREVIOUS_STATEMENT) { - const origin = this.block.getRelativeToSurfaceXY(); offset = new Coordinate( - origin.x + this.BLOCK_CONNECTION_OFFSET, - origin.y - this.BLOCK_CONNECTION_OFFSET, + neighbour.x, + neighbour.y - + this.block.getHeightWidth().height - + this.BLOCK_CONNECTION_OFFSET, ); } else { offset = new Coordinate( @@ -275,8 +272,15 @@ export class BlockDragStrategy implements IDragStrategy { } this.block.moveDuringDrag(offset); } + } else { + this.block.moveDuringDrag(this.startLoc); } + this.workspace.getLayerManager()?.moveToDragLayer(this.block); + this.getVisibleBubbles(this.block).forEach((bubble) => { + this.workspace.getLayerManager()?.moveToDragLayer(bubble, false); + }); + this.announceMove(true); return this.block; } @@ -388,7 +392,7 @@ export class BlockDragStrategy implements IDragStrategy { * the initial connection pair is also used as the first connection candidate. */ private storeInitialConnections(healStack: boolean) { - // Prioritze the block's parent connection (output or previous) if one exists. + // Prioritize the block's parent connection (output or previous) if one exists. let localParentConn: RenderedConnection | null = null; let parentTargetConn: RenderedConnection | null = null; @@ -536,7 +540,8 @@ export class BlockDragStrategy implements IDragStrategy { delta: Coordinate, ): RenderedConnection | undefined { const currCandidate = this.connectionCandidate; - const newCandidate = this.getConnectionCandidate(delta); + const newCandidate = + this.getInitialCandidate() ?? this.getConnectionCandidate(delta); if (!newCandidate) { // Position above or below the first/last block. @@ -584,6 +589,8 @@ export class BlockDragStrategy implements IDragStrategy { ? currCandidate : newCandidate; this.connectionCandidate = candidate; + console.log('updated'); + console.log(this.connectionCandidate); const {local, neighbour} = candidate; const localIsOutputOrPrevious = @@ -1049,4 +1056,74 @@ export class BlockDragStrategy implements IDragStrategy { return connections as RenderedConnection[]; } + + /** + * Returns a connection candidate to move the dragged block to at the start of + * a drag. If the passively focused node is a connection and the dragged block + * can connect to it, the connection will be returned. Otherwise, the first + * compatible connection on the passively focused node's block, if any, will + * be returned. Returns null if the workspace does not have passive focus. + */ + private getInitialCandidate(): ConnectionCandidate | null { + const passiveElement = this.workspace + .getSvgGroup() + .querySelector(`.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`); + if ( + !passiveElement || + !passiveElement.id || + passiveElement === this.block.getFocusableElement() + ) { + return null; + } + const passiveNode = this.workspace.lookUpFocusableNode(passiveElement.id); + if (!passiveNode) return null; + + const passiveBlock = this.workspace + .getNavigator() + .getSourceBlockFromNode(passiveNode); + if (!passiveBlock) return null; + + const passiveBlockConnections = passiveBlock.getConnections_(false); + let passiveConnection: RenderedConnection | null = null; + + // If the passively focused node is a connection, return it if it is + // compatible with the dragged block. + if (passiveBlockConnections.includes(passiveNode as any)) { + passiveConnection = passiveNode as RenderedConnection; + const connectionChecker = this.block.workspace.connectionChecker; + const local = this.block.getConnections_(false).find((connection) => { + return connectionChecker.canConnect( + connection, + passiveConnection, + true, + Infinity, + ); + }); + + if (local) { + return {local, neighbour: passiveConnection, distance: 0}; + } + } + + // Fall back to returning the first compatible connection on the passively + // focused block, if any. + const pair = this.allConnectionPairs.find( + (pair) => pair.neighbour.getSourceBlock() === passiveBlock, + ); + if (pair) { + return this.pairToCandidate(pair); + } + + // Fall back further to the parent connection of the passively focused + // block, if any. + const outputTarget = passiveBlock.outputConnection?.targetConnection; + const parentPair = this.allConnectionPairs.find( + (pair) => pair.neighbour === outputTarget, + ); + if (parentPair) { + return this.pairToCandidate(parentPair); + } + + return null; + } } diff --git a/packages/blockly/tests/mocha/keyboard_movement_test.js b/packages/blockly/tests/mocha/keyboard_movement_test.js index 21d1cc55e3d..add8573c1da 100644 --- a/packages/blockly/tests/mocha/keyboard_movement_test.js +++ b/packages/blockly/tests/mocha/keyboard_movement_test.js @@ -18,10 +18,29 @@ import { } from './test_helpers/setup_teardown.js'; import {createKeyDownEvent} from './test_helpers/user_input.js'; -suite('Keyboard-driven movement', function () { +suite.only('Keyboard-driven movement', function () { setup(function () { sharedTestSetup.call(this); - const toolbox = document.getElementById('toolbox-simple'); + const toolbox = { + // There are two kinds of toolboxes. The simpler one is a flyout toolbox. + kind: 'flyoutToolbox', + // The contents is the blocks and other items that exist in your toolbox. + contents: [ + { + kind: 'block', + type: 'text_print', + }, + { + kind: 'block', + type: 'logic_negate', + }, + { + kind: 'block', + type: 'math_number', + }, + ], + }; + this.workspace = Blockly.inject('blocklyDiv', {toolbox: toolbox}); Blockly.common.defineBlocks(p5blocks); Blockly.KeyboardMover.mover.setMoveDistance(20); @@ -554,6 +573,124 @@ suite('Keyboard-driven movement', function () { toastSpy.restore(); }); + test('initially moves the block to the previously-focused statement connection', function () { + const ifBlock = this.workspace.newBlock('controls_if'); + ifBlock.initSvg(); + ifBlock.render(); + + const statementConnection = ifBlock.getInput('DO0').connection; + Blockly.getFocusManager().focusNode(statementConnection); + const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); + this.workspace.getInjectionDiv().dispatchEvent(t); + const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + this.workspace.getInjectionDiv().dispatchEvent(enter); + + const movingBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = movingBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, movingBlock.previousConnection); + assert.equal(candidate.neighbour, statementConnection); + + const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); + this.workspace.getInjectionDiv().dispatchEvent(esc); + }); + + test("initially moves the block to the previously-focused block's previous connection", function () { + const ifBlock = this.workspace.newBlock('controls_if'); + ifBlock.initSvg(); + ifBlock.render(); + + Blockly.getFocusManager().focusNode(ifBlock); + const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); + this.workspace.getInjectionDiv().dispatchEvent(t); + const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + this.workspace.getInjectionDiv().dispatchEvent(enter); + + const movingBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = movingBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, movingBlock.nextConnection); + assert.equal(candidate.neighbour, ifBlock.previousConnection); + + const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); + this.workspace.getInjectionDiv().dispatchEvent(esc); + }); + + test('initially moves the block to the previously-focused input connection', function () { + const ifBlock = this.workspace.newBlock('controls_if'); + ifBlock.initSvg(); + ifBlock.render(); + + const inputConnection = ifBlock.getInput('IF0').connection; + Blockly.getFocusManager().focusNode(inputConnection); + const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); + this.workspace.getInjectionDiv().dispatchEvent(t); + const down = createKeyDownEvent(Blockly.utils.KeyCodes.DOWN); + this.workspace.getInjectionDiv().dispatchEvent(down); + const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + this.workspace.getInjectionDiv().dispatchEvent(enter); + + const movingBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = movingBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, movingBlock.outputConnection); + assert.equal(candidate.neighbour, inputConnection); + + const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); + this.workspace.getInjectionDiv().dispatchEvent(esc); + }); + + test("initially moves the block to the previously-focused block's input connection", function () { + const ifBlock = this.workspace.newBlock('controls_if'); + ifBlock.initSvg(); + ifBlock.render(); + + Blockly.getFocusManager().focusNode(ifBlock); + const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); + this.workspace.getInjectionDiv().dispatchEvent(t); + const down = createKeyDownEvent(Blockly.utils.KeyCodes.DOWN); + this.workspace.getInjectionDiv().dispatchEvent(down); + const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + this.workspace.getInjectionDiv().dispatchEvent(enter); + + const movingBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = movingBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, movingBlock.outputConnection); + assert.equal(candidate.neighbour, ifBlock.getInput('IF0').connection); + + const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); + this.workspace.getInjectionDiv().dispatchEvent(esc); + }); + + test("initially moves the block to the previously-focused block's parent input connection", function () { + const compare = this.workspace.newBlock('logic_compare'); + compare.initSvg(); + compare.render(); + + const boolean = this.workspace.newBlock('logic_boolean'); + boolean.initSvg(); + boolean.render(); + boolean.outputConnection.connect(compare.getInput('A').connection); + + Blockly.getFocusManager().focusNode(boolean); + const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); + this.workspace.getInjectionDiv().dispatchEvent(t); + const down = createKeyDownEvent(Blockly.utils.KeyCodes.DOWN); + this.workspace.getInjectionDiv().dispatchEvent(down); + const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); + this.workspace.getInjectionDiv().dispatchEvent(enter); + + const movingBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = movingBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, movingBlock.outputConnection); + assert.equal(candidate.neighbour, compare.getInput('A').connection); + + const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); + this.workspace.getInjectionDiv().dispatchEvent(esc); + }); + suite('Statement move tests', function () { // Clear the workspace and load start blocks. setup(function () { From 6cd65f01684ee514d91f30ac6d3de63ee4d9b1b9 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 1 May 2026 15:51:36 -0700 Subject: [PATCH 2/6] chore: Fix import --- packages/blockly/core/dragging/block_drag_strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blockly/core/dragging/block_drag_strategy.ts b/packages/blockly/core/dragging/block_drag_strategy.ts index 6c6c531eb6d..276c55a5a80 100644 --- a/packages/blockly/core/dragging/block_drag_strategy.ts +++ b/packages/blockly/core/dragging/block_drag_strategy.ts @@ -27,7 +27,7 @@ import * as layers from '../layers.js'; import {Msg} from '../msg.js'; import * as registry from '../registry.js'; import {finishQueuedRenders} from '../render_management.js'; -import {RenderedConnection} from '../rendered_connection.js'; +import type {RenderedConnection} from '../rendered_connection.js'; import * as blocks from '../serialization/blocks.js'; import {Coordinate} from '../utils.js'; import * as aria from '../utils/aria.js'; From febf17d44a0e73b2fe7f1ab831b0eb11a65ab8bd Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Fri, 1 May 2026 16:03:14 -0700 Subject: [PATCH 3/6] chore: Remove errant `.only` --- packages/blockly/tests/mocha/keyboard_movement_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blockly/tests/mocha/keyboard_movement_test.js b/packages/blockly/tests/mocha/keyboard_movement_test.js index add8573c1da..fa5969aa266 100644 --- a/packages/blockly/tests/mocha/keyboard_movement_test.js +++ b/packages/blockly/tests/mocha/keyboard_movement_test.js @@ -18,7 +18,7 @@ import { } from './test_helpers/setup_teardown.js'; import {createKeyDownEvent} from './test_helpers/user_input.js'; -suite.only('Keyboard-driven movement', function () { +suite('Keyboard-driven movement', function () { setup(function () { sharedTestSetup.call(this); const toolbox = { From df4e3b05fdbdd14eee7a15e37f1d2c8705c877c6 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 5 May 2026 08:27:03 -0700 Subject: [PATCH 4/6] chore: Remove errant logging --- packages/blockly/core/dragging/block_drag_strategy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/blockly/core/dragging/block_drag_strategy.ts b/packages/blockly/core/dragging/block_drag_strategy.ts index 276c55a5a80..5491d727df6 100644 --- a/packages/blockly/core/dragging/block_drag_strategy.ts +++ b/packages/blockly/core/dragging/block_drag_strategy.ts @@ -589,8 +589,6 @@ export class BlockDragStrategy implements IDragStrategy { ? currCandidate : newCandidate; this.connectionCandidate = candidate; - console.log('updated'); - console.log(this.connectionCandidate); const {local, neighbour} = candidate; const localIsOutputOrPrevious = From bff18c7eced615c043f26ff02cd454528a8c18aa Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 5 May 2026 10:00:34 -0700 Subject: [PATCH 5/6] refactor: Clarify tests --- .../tests/mocha/keyboard_movement_test.js | 91 ++++++++----------- 1 file changed, 39 insertions(+), 52 deletions(-) diff --git a/packages/blockly/tests/mocha/keyboard_movement_test.js b/packages/blockly/tests/mocha/keyboard_movement_test.js index fa5969aa266..ab7e363313b 100644 --- a/packages/blockly/tests/mocha/keyboard_movement_test.js +++ b/packages/blockly/tests/mocha/keyboard_movement_test.js @@ -92,6 +92,11 @@ suite('Keyboard-driven movement', function () { workspace.getInjectionDiv().dispatchEvent(event); } + function focusToolbox(workspace) { + const event = createKeyDownEvent(Blockly.utils.KeyCodes.T); + workspace.getInjectionDiv().dispatchEvent(event); + } + /** * Create a new block from serialised state (parsed JSON) and * optionally attach it to an existing block on the workspace. @@ -551,7 +556,7 @@ suite('Keyboard-driven movement', function () { testExemptedShortcutsAllowed(); }); - suite('in constrained mode', function () { + suite.only('in constrained mode', function () { test('prompts to use unconstrained mode when no destinations are available', function () { const toastSpy = sinon.spy(Blockly.Toast, 'show'); const beepSpy = sinon.spy(this.workspace.getAudioManager(), 'beep'); @@ -580,19 +585,16 @@ suite('Keyboard-driven movement', function () { const statementConnection = ifBlock.getInput('DO0').connection; Blockly.getFocusManager().focusNode(statementConnection); - const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); - this.workspace.getInjectionDiv().dispatchEvent(t); - const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); - this.workspace.getInjectionDiv().dispatchEvent(enter); + focusToolbox(this.workspace); + startMove(this.workspace); - const movingBlock = Blockly.getFocusManager().getFocusedNode(); - const candidate = movingBlock.getDragStrategy().connectionCandidate; + const printBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = printBlock.getDragStrategy().connectionCandidate; - assert.equal(candidate.local, movingBlock.previousConnection); + assert.equal(candidate.local, printBlock.previousConnection); assert.equal(candidate.neighbour, statementConnection); - const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); - this.workspace.getInjectionDiv().dispatchEvent(esc); + cancelMove(this.workspace); }); test("initially moves the block to the previously-focused block's previous connection", function () { @@ -601,19 +603,16 @@ suite('Keyboard-driven movement', function () { ifBlock.render(); Blockly.getFocusManager().focusNode(ifBlock); - const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); - this.workspace.getInjectionDiv().dispatchEvent(t); - const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); - this.workspace.getInjectionDiv().dispatchEvent(enter); + focusToolbox(this.workspace); + startMove(this.workspace) - const movingBlock = Blockly.getFocusManager().getFocusedNode(); - const candidate = movingBlock.getDragStrategy().connectionCandidate; + const printBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = printBlock.getDragStrategy().connectionCandidate; - assert.equal(candidate.local, movingBlock.nextConnection); + assert.equal(candidate.local, printBlock.nextConnection); assert.equal(candidate.neighbour, ifBlock.previousConnection); - const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); - this.workspace.getInjectionDiv().dispatchEvent(esc); + cancelMove(this.workspace); }); test('initially moves the block to the previously-focused input connection', function () { @@ -623,21 +622,17 @@ suite('Keyboard-driven movement', function () { const inputConnection = ifBlock.getInput('IF0').connection; Blockly.getFocusManager().focusNode(inputConnection); - const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); - this.workspace.getInjectionDiv().dispatchEvent(t); - const down = createKeyDownEvent(Blockly.utils.KeyCodes.DOWN); - this.workspace.getInjectionDiv().dispatchEvent(down); - const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); - this.workspace.getInjectionDiv().dispatchEvent(enter); + focusToolbox(this.workspace); + moveDown(this.workspace); + startMove(this.workspace); - const movingBlock = Blockly.getFocusManager().getFocusedNode(); - const candidate = movingBlock.getDragStrategy().connectionCandidate; + const notBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = notBlock.getDragStrategy().connectionCandidate; - assert.equal(candidate.local, movingBlock.outputConnection); + assert.equal(candidate.local, notBlock.outputConnection); assert.equal(candidate.neighbour, inputConnection); - const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); - this.workspace.getInjectionDiv().dispatchEvent(esc); + cancelMove(this.workspace); }); test("initially moves the block to the previously-focused block's input connection", function () { @@ -646,21 +641,17 @@ suite('Keyboard-driven movement', function () { ifBlock.render(); Blockly.getFocusManager().focusNode(ifBlock); - const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); - this.workspace.getInjectionDiv().dispatchEvent(t); - const down = createKeyDownEvent(Blockly.utils.KeyCodes.DOWN); - this.workspace.getInjectionDiv().dispatchEvent(down); - const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); - this.workspace.getInjectionDiv().dispatchEvent(enter); + focusToolbox(this.workspace); + moveDown(this.workspace); + startMove(this.workspace); - const movingBlock = Blockly.getFocusManager().getFocusedNode(); - const candidate = movingBlock.getDragStrategy().connectionCandidate; + const notBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = notBlock.getDragStrategy().connectionCandidate; - assert.equal(candidate.local, movingBlock.outputConnection); + assert.equal(candidate.local, notBlock.outputConnection); assert.equal(candidate.neighbour, ifBlock.getInput('IF0').connection); - const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); - this.workspace.getInjectionDiv().dispatchEvent(esc); + cancelMove(this.workspace); }); test("initially moves the block to the previously-focused block's parent input connection", function () { @@ -674,21 +665,17 @@ suite('Keyboard-driven movement', function () { boolean.outputConnection.connect(compare.getInput('A').connection); Blockly.getFocusManager().focusNode(boolean); - const t = createKeyDownEvent(Blockly.utils.KeyCodes.T); - this.workspace.getInjectionDiv().dispatchEvent(t); - const down = createKeyDownEvent(Blockly.utils.KeyCodes.DOWN); - this.workspace.getInjectionDiv().dispatchEvent(down); - const enter = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER); - this.workspace.getInjectionDiv().dispatchEvent(enter); + focusToolbox(this.workspace); + moveDown(this.workspace); + startMove(this.workspace); - const movingBlock = Blockly.getFocusManager().getFocusedNode(); - const candidate = movingBlock.getDragStrategy().connectionCandidate; + const notBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = notBlock.getDragStrategy().connectionCandidate; - assert.equal(candidate.local, movingBlock.outputConnection); + assert.equal(candidate.local, notBlock.outputConnection); assert.equal(candidate.neighbour, compare.getInput('A').connection); - const esc = createKeyDownEvent(Blockly.utils.KeyCodes.ESC); - this.workspace.getInjectionDiv().dispatchEvent(esc); + cancelMove(this.workspace); }); suite('Statement move tests', function () { From 7a66e6a470de420941718063b6042baf2a8eeba4 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Tue, 5 May 2026 10:21:08 -0700 Subject: [PATCH 6/6] test: Add additional tests --- .../tests/mocha/keyboard_movement_test.js | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/blockly/tests/mocha/keyboard_movement_test.js b/packages/blockly/tests/mocha/keyboard_movement_test.js index ab7e363313b..1bbb4b94542 100644 --- a/packages/blockly/tests/mocha/keyboard_movement_test.js +++ b/packages/blockly/tests/mocha/keyboard_movement_test.js @@ -556,7 +556,7 @@ suite('Keyboard-driven movement', function () { testExemptedShortcutsAllowed(); }); - suite.only('in constrained mode', function () { + suite('in constrained mode', function () { test('prompts to use unconstrained mode when no destinations are available', function () { const toastSpy = sinon.spy(Blockly.Toast, 'show'); const beepSpy = sinon.spy(this.workspace.getAudioManager(), 'beep'); @@ -604,7 +604,7 @@ suite('Keyboard-driven movement', function () { Blockly.getFocusManager().focusNode(ifBlock); focusToolbox(this.workspace); - startMove(this.workspace) + startMove(this.workspace); const printBlock = Blockly.getFocusManager().getFocusedNode(); const candidate = printBlock.getDragStrategy().connectionCandidate; @@ -635,6 +635,25 @@ suite('Keyboard-driven movement', function () { cancelMove(this.workspace); }); + test('initially moves the block to the previously-focused not-first statement connection', function () { + const ifBlock = this.workspace.newBlock('controls_ifelse'); + ifBlock.initSvg(); + ifBlock.render(); + + const statementConnection = ifBlock.getInput('ELSE').connection; + Blockly.getFocusManager().focusNode(statementConnection); + focusToolbox(this.workspace); + startMove(this.workspace); + + const printBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = printBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, printBlock.previousConnection); + assert.equal(candidate.neighbour, statementConnection); + + cancelMove(this.workspace); + }); + test("initially moves the block to the previously-focused block's input connection", function () { const ifBlock = this.workspace.newBlock('controls_if'); ifBlock.initSvg(); @@ -654,6 +673,25 @@ suite('Keyboard-driven movement', function () { cancelMove(this.workspace); }); + test('initially moves the block to the previously-focused not-first input connection', function () { + const compare = this.workspace.newBlock('logic_compare'); + compare.initSvg(); + compare.render(); + + Blockly.getFocusManager().focusNode(compare.getInput('B').connection); + focusToolbox(this.workspace); + moveDown(this.workspace); + startMove(this.workspace); + + const notBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = notBlock.getDragStrategy().connectionCandidate; + + assert.equal(candidate.local, notBlock.outputConnection); + assert.equal(candidate.neighbour, compare.getInput('B').connection); + + cancelMove(this.workspace); + }); + test("initially moves the block to the previously-focused block's parent input connection", function () { const compare = this.workspace.newBlock('logic_compare'); compare.initSvg(); @@ -678,6 +716,24 @@ suite('Keyboard-driven movement', function () { cancelMove(this.workspace); }); + test('initially moves the block to the workspace when the previously-focused block has no compatible connections', function () { + const repeat = this.workspace.newBlock('controls_repeat'); + repeat.initSvg(); + repeat.render(); + + Blockly.getFocusManager().focusNode(repeat); + focusToolbox(this.workspace); + moveDown(this.workspace); + startMove(this.workspace); + + const notBlock = Blockly.getFocusManager().getFocusedNode(); + const candidate = notBlock.getDragStrategy().connectionCandidate; + + assert.isNull(candidate); + + cancelMove(this.workspace); + }); + suite('Statement move tests', function () { // Clear the workspace and load start blocks. setup(function () {