From 9c718c4f93d217239f9101419bb0b1bbc5a11deb Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 26 Aug 2026 12:52:12 -0700 Subject: [PATCH 1/3] test: Stop leaking workspaces --- packages/blockly/tests/mocha/block_test.js | 12 +++++++ .../tests/mocha/blocks/procedures_test.js | 25 +++++++++++--- .../blockly/tests/mocha/clipboard_test.js | 25 +++++--------- .../tests/mocha/connection_checker_test.js | 11 ++++-- .../blockly/tests/mocha/connection_test.js | 4 +++ .../blockly/tests/mocha/dropdowndiv_test.js | 2 ++ .../tests/mocha/field_checkbox_test.js | 5 +++ .../tests/mocha/field_dropdown_test.js | 5 +++ .../blockly/tests/mocha/field_image_test.js | 6 ++++ .../blockly/tests/mocha/field_label_test.js | 2 ++ .../blockly/tests/mocha/field_number_test.js | 5 +++ .../tests/mocha/field_textinput_test.js | 5 +++ .../tests/mocha/field_variable_test.js | 5 +++ packages/blockly/tests/mocha/icon_test.js | 34 ++++++++++++++++--- packages/blockly/tests/mocha/json_test.js | 14 ++++---- .../tests/mocha/keyboard_movement_test.js | 5 +++ .../tests/mocha/keyboard_navigation_test.js | 17 ++++++++++ .../blockly/tests/mocha/navigation_test.js | 7 ++-- .../tests/mocha/render_management_test.js | 5 +++ .../tests/mocha/shortcut_items_test.js | 13 ++++--- .../tests/mocha/test_helpers/workspace.js | 5 ++- packages/blockly/tests/mocha/toolbox_test.js | 7 +++- .../blockly/tests/mocha/workspace_svg_test.js | 19 ++++++----- 23 files changed, 188 insertions(+), 50 deletions(-) diff --git a/packages/blockly/tests/mocha/block_test.js b/packages/blockly/tests/mocha/block_test.js index 2f624c94b94..659894bc31b 100644 --- a/packages/blockly/tests/mocha/block_test.js +++ b/packages/blockly/tests/mocha/block_test.js @@ -2963,6 +2963,11 @@ suite('Blocks', function () { block.render(); } }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + test('Bubbles are moved to drag layer along with their blocks', async function () { this.blocks.A.setCommentText('a'); this.blocks.B.setCommentText('b'); @@ -3007,6 +3012,12 @@ suite('Blocks', function () { firstBlock.moveBy(-500, -500); }); + teardown(function () { + if (this.workspace) { + workspaceTeardown.call(this, this.workspace); + } + }); + test('Deleting the sole block on the workspace focuses the workspace', function () { const block = this.workspace.getTopBlocks(false)[0]; Blockly.getFocusManager().focusNode(block); @@ -3111,6 +3122,7 @@ suite('Blocks', function () { this.workspace.getTopBlocks(false)[0], ); this.workspace.dispose(); + this.workspace = null; this.clock.runAll(); // No assert, this just shouldn't throw. diff --git a/packages/blockly/tests/mocha/blocks/procedures_test.js b/packages/blockly/tests/mocha/blocks/procedures_test.js index a43b3c1e5cd..e4f33ab6114 100644 --- a/packages/blockly/tests/mocha/blocks/procedures_test.js +++ b/packages/blockly/tests/mocha/blocks/procedures_test.js @@ -1930,14 +1930,23 @@ suite('Procedures', function () { const statementField = containerBlock.getField('STATEMENTS'); statementField.setValue(value); defBlock.compose(containerBlock); + workspaceTeardown.call(this, mutatorWorkspace); } if (testSuite.defType === 'procedures_defreturn') { test('Has Statements', function () { - setStatementValue(this.workspace, this.defBlock, true); + setStatementValue.apply(this, [ + this.workspace, + this.defBlock, + true, + ]); assert.isTrue(this.defBlock.hasStatements_); }); test('Has No Statements', function () { - setStatementValue(this.workspace, this.defBlock, false); + setStatementValue.apply(this, [ + this.workspace, + this.defBlock, + false, + ]); assert.isFalse(this.defBlock.hasStatements_); }); test('Saving Statements', function () { @@ -1952,9 +1961,13 @@ suite('Procedures', function () { blockXml, this.workspace, ); - setStatementValue(this.workspace, defBlock, false); + setStatementValue.apply(this, [ + this.workspace, + defBlock, + false, + ]); assert.isNull(defBlock.getInput('STACK')); - setStatementValue(this.workspace, defBlock, true); + setStatementValue.apply(this, [this.workspace, defBlock, true]); assert.isNotNull(defBlock.getInput('STACK')); const statementBlocks = defBlock.getChildren(); assert.equal(statementBlocks.length, 1); @@ -2064,6 +2077,7 @@ suite('Procedures', function () { .getTopBlocks()[0] .getInput('STATEMENT_INPUT'); assert.isNotNull(statementInput); + workspaceTeardown.call(this, mutatorWorkspace); }); test('Has Statements', function () { this.defBlock.hasStatements_ = true; @@ -2078,6 +2092,7 @@ suite('Procedures', function () { .getField('STATEMENTS') .getValueBoolean(); assert.isTrue(statementValue); + workspaceTeardown.call(this, mutatorWorkspace); }); test('No Has Statements', function () { this.defBlock.hasStatements_ = false; @@ -2092,6 +2107,7 @@ suite('Procedures', function () { .getField('STATEMENTS') .getValueBoolean(); assert.isFalse(statementValue); + workspaceTeardown.call(this, mutatorWorkspace); }); } else { test('Has no Statement Input', function () { @@ -2105,6 +2121,7 @@ suite('Procedures', function () { .getTopBlocks()[0] .getInput('STATEMENT_INPUT'); assert.isNull(statementInput); + workspaceTeardown.call(this, mutatorWorkspace); }); } }); diff --git a/packages/blockly/tests/mocha/clipboard_test.js b/packages/blockly/tests/mocha/clipboard_test.js index 56dba56e1eb..80b7254e9ad 100644 --- a/packages/blockly/tests/mocha/clipboard_test.js +++ b/packages/blockly/tests/mocha/clipboard_test.js @@ -159,8 +159,7 @@ suite('Clipboard', function () { }); test('pasted blocks are bumped to not overlap in RTL', function () { - this.workspace.dispose(); - this.workspace = Blockly.inject('blocklyDiv', { + const workspace = Blockly.inject('blocklyDiv', { ...DEFAULT_INJECT_OPTIONS, rtl: true, }); @@ -170,11 +169,11 @@ suite('Clipboard', function () { 'x': 38, 'y': 13, }, - this.workspace, + workspace, ); const data = block.toCopyData(); - const newBlock = Blockly.clipboard.paste(data, this.workspace); + const newBlock = Blockly.clipboard.paste(data, workspace); const oldBlockXY = block.getRelativeToSurfaceXY(); assert.deepEqual( newBlock.getRelativeToSurfaceXY(), @@ -183,10 +182,7 @@ suite('Clipboard', function () { oldBlockXY.y + Blockly.config.snapRadius * 2, ), ); - - // Restore an LTR workspace. - this.workspace.dispose(); - this.workspace = Blockly.inject('blocklyDiv', DEFAULT_INJECT_OPTIONS); + workspace.dispose(); }); test('pasted blocks are bumped to be outside the connection snap radius', function () { @@ -242,8 +238,7 @@ suite('Clipboard', function () { }); test('pasted comments are bumped to not overlap in RTL', function () { - this.workspace.dispose(); - this.workspace = Blockly.inject('blocklyDiv', { + const workspace = Blockly.inject('blocklyDiv', { ...DEFAULT_INJECT_OPTIONS, rtl: true, }); @@ -251,20 +246,18 @@ suite('Clipboard', function () { Blockly.utils.xml.textToDom( '', ), - this.workspace, + workspace, ); - const comment = this.workspace.getTopComments(false)[0]; + const comment = workspace.getTopComments(false)[0]; const data = comment.toCopyData(); - const newComment = Blockly.clipboard.paste(data, this.workspace); + const newComment = Blockly.clipboard.paste(data, workspace); const oldCommentXY = comment.getRelativeToSurfaceXY(); assert.deepEqual( newComment.getRelativeToSurfaceXY(), new Blockly.utils.Coordinate(oldCommentXY.x - 30, oldCommentXY.y + 30), ); - // Restore an LTR workspace. - this.workspace.dispose(); - this.workspace = Blockly.inject('blocklyDiv', DEFAULT_INJECT_OPTIONS); + workspace.dispose(); }); }); }); diff --git a/packages/blockly/tests/mocha/connection_checker_test.js b/packages/blockly/tests/mocha/connection_checker_test.js index 6bc5f56e059..6d74baf8b08 100644 --- a/packages/blockly/tests/mocha/connection_checker_test.js +++ b/packages/blockly/tests/mocha/connection_checker_test.js @@ -10,6 +10,7 @@ import { DEFAULT_INJECT_OPTIONS, sharedTestSetup, sharedTestTeardown, + workspaceTeardown, } from './test_helpers/setup_teardown.js'; suite('Connection checker', function () { @@ -514,9 +515,16 @@ suite('Connection checker', function () { }); }); suite('Dragging Checks', function () { + setup(function () { + this.workspace = Blockly.inject('blocklyDiv', DEFAULT_INJECT_OPTIONS); + }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + suite('Stacks', function () { setup(function () { - this.workspace = Blockly.inject('blocklyDiv', DEFAULT_INJECT_OPTIONS); // Load in three blocks: A and B are connected (next/prev); B is unmovable. Blockly.Xml.domToWorkspace( Blockly.utils.xml @@ -627,7 +635,6 @@ suite('Connection checker', function () { }); suite('Rows', function () { setup(function () { - this.workspace = Blockly.inject('blocklyDiv', DEFAULT_INJECT_OPTIONS); // Load 3 blocks: A and B are connected (input/output); B is unmovable. Blockly.Xml.domToWorkspace( Blockly.utils.xml diff --git a/packages/blockly/tests/mocha/connection_test.js b/packages/blockly/tests/mocha/connection_test.js index 273ffe2230a..6d749dd21c7 100644 --- a/packages/blockly/tests/mocha/connection_test.js +++ b/packages/blockly/tests/mocha/connection_test.js @@ -2990,6 +2990,10 @@ suite('Connection', function () { }; }); + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + suite('Disconnect from old parent', function () { test('Value', function () { const oldParent = this.workspace.newBlock('row_block'); diff --git a/packages/blockly/tests/mocha/dropdowndiv_test.js b/packages/blockly/tests/mocha/dropdowndiv_test.js index cb6e119bc4d..53998aaa0b5 100644 --- a/packages/blockly/tests/mocha/dropdowndiv_test.js +++ b/packages/blockly/tests/mocha/dropdowndiv_test.js @@ -11,6 +11,7 @@ import { DEFAULT_INJECT_OPTIONS, sharedTestSetup, sharedTestTeardown, + workspaceTeardown, } from './test_helpers/setup_teardown.js'; suite('DropDownDiv', function () { @@ -155,6 +156,7 @@ suite('DropDownDiv', function () { }); teardown(function () { this.boundsStub.restore(); + workspaceTeardown.call(this, this.workspace); }); test('Escape dismisses DropDownDiv', function () { let hidden = false; diff --git a/packages/blockly/tests/mocha/field_checkbox_test.js b/packages/blockly/tests/mocha/field_checkbox_test.js index 721d1aa2251..781c10a0c93 100644 --- a/packages/blockly/tests/mocha/field_checkbox_test.js +++ b/packages/blockly/tests/mocha/field_checkbox_test.js @@ -308,6 +308,11 @@ suite('Checkbox Fields', function () { this.focusableElement = this.field.getClickTarget_(); }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + test('Block has field type name in ARIA label', function () { const blockLabel = this.block.getAriaLabel(); assert.include(blockLabel, 'checkbox'); diff --git a/packages/blockly/tests/mocha/field_dropdown_test.js b/packages/blockly/tests/mocha/field_dropdown_test.js index 4bcc26d0cdc..6423af87141 100644 --- a/packages/blockly/tests/mocha/field_dropdown_test.js +++ b/packages/blockly/tests/mocha/field_dropdown_test.js @@ -334,6 +334,11 @@ suite('Dropdown Fields', function () { renderer: 'geras', }); }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + suite('Simple Dropdown', function () { setup(function () { this.block = this.workspace.newBlock('logic_boolean'); diff --git a/packages/blockly/tests/mocha/field_image_test.js b/packages/blockly/tests/mocha/field_image_test.js index 0ffd678b557..7e243cc0492 100644 --- a/packages/blockly/tests/mocha/field_image_test.js +++ b/packages/blockly/tests/mocha/field_image_test.js @@ -16,6 +16,7 @@ import { DEFAULT_INJECT_OPTIONS, sharedTestSetup, sharedTestTeardown, + workspaceTeardown, } from './test_helpers/setup_teardown.js'; suite('Image Fields', function () { @@ -364,6 +365,11 @@ suite('Image Fields', function () { renderer: 'geras', }); }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + suite('Image without click handler', function () { setup(function () { this.block = this.workspace.newBlock('text'); diff --git a/packages/blockly/tests/mocha/field_label_test.js b/packages/blockly/tests/mocha/field_label_test.js index 485dd0c0583..7ff62490413 100644 --- a/packages/blockly/tests/mocha/field_label_test.js +++ b/packages/blockly/tests/mocha/field_label_test.js @@ -17,6 +17,7 @@ import { DEFAULT_INJECT_OPTIONS, sharedTestSetup, sharedTestTeardown, + workspaceTeardown, } from './test_helpers/setup_teardown.js'; suite('Label Fields', function () { @@ -239,6 +240,7 @@ suite('Label Fields', function () { const focusableElement = field.getFocusableElement(); const ariaHidden = focusableElement.getAttribute('aria-hidden'); assert.equal(ariaHidden, 'true'); + workspaceTeardown.call(this, workspace); }); }); }); diff --git a/packages/blockly/tests/mocha/field_number_test.js b/packages/blockly/tests/mocha/field_number_test.js index ce5649b214f..70275ff7519 100644 --- a/packages/blockly/tests/mocha/field_number_test.js +++ b/packages/blockly/tests/mocha/field_number_test.js @@ -516,6 +516,11 @@ suite('Number Fields', function () { this.focusableElement = this.field.getClickTarget_(); }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + test('Field has field type name in ARIA label', function () { const fieldLabel = this.focusableElement.getAttribute('aria-label'); assert.include(fieldLabel, 'number:'); diff --git a/packages/blockly/tests/mocha/field_textinput_test.js b/packages/blockly/tests/mocha/field_textinput_test.js index 36612146831..612d5b0d913 100644 --- a/packages/blockly/tests/mocha/field_textinput_test.js +++ b/packages/blockly/tests/mocha/field_textinput_test.js @@ -636,6 +636,11 @@ suite('Text Input Fields', function () { this.focusableElement = this.field.getClickTarget_(); }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + test('Field has field type name in ARIA label', function () { const fieldLabel = this.focusableElement.getAttribute('aria-label'); assert.include(fieldLabel, 'text:'); diff --git a/packages/blockly/tests/mocha/field_variable_test.js b/packages/blockly/tests/mocha/field_variable_test.js index a6129c13a5b..31009de1cfc 100644 --- a/packages/blockly/tests/mocha/field_variable_test.js +++ b/packages/blockly/tests/mocha/field_variable_test.js @@ -663,6 +663,11 @@ suite('Variable Fields', function () { this.focusableElement = this.field.getFocusableElement(); }); + + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + test('Block has "Variable" qualifier in ARIA label', function () { const blockLabel = this.block.getAriaLabel(); assert.include(blockLabel, 'Variable'); diff --git a/packages/blockly/tests/mocha/icon_test.js b/packages/blockly/tests/mocha/icon_test.js index de4d8e0d769..67e45635c9a 100644 --- a/packages/blockly/tests/mocha/icon_test.js +++ b/packages/blockly/tests/mocha/icon_test.js @@ -12,6 +12,7 @@ import { DEFAULT_INJECT_OPTIONS, sharedTestSetup, sharedTestTeardown, + workspaceTeardown, } from './test_helpers/setup_teardown.js'; import {simulateClick} from './test_helpers/user_input.js'; @@ -89,6 +90,8 @@ suite('Icon', function () { initViewSpy.called, 'Expected initView to not be called', ); + + workspaceTeardown.call(this, workspace); }); test( @@ -106,6 +109,7 @@ suite('Icon', function () { initViewSpy.calledOnce, 'Expected initView to be called', ); + workspaceTeardown.call(this, workspace); }, ); }); @@ -123,6 +127,7 @@ suite('Icon', function () { applyColourSpy.called, 'Expected applyColour to not be called', ); + workspaceTeardown.call(this, workspace); }); test( @@ -139,6 +144,7 @@ suite('Icon', function () { applyColourSpy.calledOnce, 'Expected applyColour to be called', ); + workspaceTeardown.call(this, workspace); }, ); @@ -155,6 +161,7 @@ suite('Icon', function () { applyColourSpy.calledOnce, 'Expected applyColour to be called', ); + workspaceTeardown.call(this, workspace); }); test("applyColour is called when the block's style changes", function () { @@ -170,6 +177,7 @@ suite('Icon', function () { applyColourSpy.calledOnce, 'Expected applyColour to be called', ); + workspaceTeardown.call(this, workspace); }); test('applyColour is called when the block is disabled', function () { @@ -185,6 +193,7 @@ suite('Icon', function () { applyColourSpy.calledOnce, 'Expected applyColour to be called', ); + workspaceTeardown.call(this, workspace); }); test('applyColour is called when the block becomes a shadow', function () { @@ -200,6 +209,7 @@ suite('Icon', function () { applyColourSpy.calledOnce, 'Expected applyColour to be called', ); + workspaceTeardown.call(this, workspace); }); }); @@ -216,6 +226,7 @@ suite('Icon', function () { updateEditableSpy.called, 'Expected updateEditable to not be called', ); + workspaceTeardown.call(this, workspace); }); test( @@ -232,6 +243,7 @@ suite('Icon', function () { updateEditableSpy.calledOnce, 'Expected updateEditable to be called', ); + workspaceTeardown.call(this, workspace); }, ); @@ -248,6 +260,7 @@ suite('Icon', function () { updateEditableSpy.calledOnce, 'Expected updateEditable to be called', ); + workspaceTeardown.call(this, workspace); }); test('updateEditable is called when the block is made editable', function () { @@ -264,6 +277,7 @@ suite('Icon', function () { updateEditableSpy.calledOnce, 'Expected updateEditable to be called', ); + workspaceTeardown.call(this, workspace); }); }); @@ -282,6 +296,7 @@ suite('Icon', function () { updateCollapsedSpy.called, 'Expected updateCollapsed to not be called', ); + workspaceTeardown.call(this, workspace); }); test('updateCollapsed is called when the block is collapsed', function () { @@ -299,6 +314,7 @@ suite('Icon', function () { updateCollapsedSpy.called, 'Expected updateCollapsed to be called', ); + workspaceTeardown.call(this, workspace); }); test('updateCollapsed is called when the block is expanded', function () { @@ -316,13 +332,15 @@ suite('Icon', function () { updateCollapsedSpy.called, 'Expected updateCollapsed to be called', ); + workspaceTeardown.call(this, workspace); }); }); }); suite('Serialization', function () { test('serializable icons are saved', function () { - const block = createHeadlessBlock(createHeadlessWorkspace()); + const workspace = createHeadlessWorkspace(); + const block = createHeadlessBlock(workspace); block.addIcon(new MockSerializableIcon()); const json = Blockly.serialization.blocks.save(block); assert.deepNestedInclude( @@ -331,10 +349,12 @@ suite('Icon', function () { 'Expected the JSON to include the saved state of the ' + 'serializable icon.', ); + workspaceTeardown.call(this, workspace); }); test('non-serializable icons are not saved', function () { - const block = createHeadlessBlock(createHeadlessWorkspace()); + const workspace = createHeadlessWorkspace(); + const block = createHeadlessBlock(workspace); block.addIcon(new MockNonSerializableIcon()); const json = Blockly.serialization.blocks.save(block); assert.notProperty( @@ -342,6 +362,7 @@ suite('Icon', function () { 'icons', 'Expected the JSON to not include any saved state for icons', ); + workspaceTeardown.call(this, workspace); }); }); @@ -368,6 +389,7 @@ suite('Icon', function () { ); Blockly.icons.registry.unregister('serializable icon'); + workspaceTeardown.call(this, workspace); }); test('trying to deserialize an unregistered icon throws an error', function () { @@ -386,6 +408,7 @@ suite('Icon', function () { '', 'Expected deserializing an unregistered icon to throw', ); + workspaceTeardown.call(this, workspace); }); }); @@ -437,12 +460,15 @@ suite('Icon', function () { }); suite('ARIA', function () { setup(function () { - const workspace = createWorkspaceSvg(); - const block = createInitializedBlock(workspace); + this.workspace = createWorkspaceSvg(); + const block = createInitializedBlock(this.workspace); const icon = new TestIcon(block); block.addIcon(icon); this.element = icon.getFocusableElement(); }); + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); test('Generic icons use button role', function () { const role = this.element.getAttribute('role'); assert.equal(role, 'button'); diff --git a/packages/blockly/tests/mocha/json_test.js b/packages/blockly/tests/mocha/json_test.js index d1fccb6db96..05f1b2b28ad 100644 --- a/packages/blockly/tests/mocha/json_test.js +++ b/packages/blockly/tests/mocha/json_test.js @@ -15,7 +15,7 @@ import {assertNoWarnings, assertWarnings} from './test_helpers/warnings.js'; suite('JSON Block Definitions', function () { setup(function () { sharedTestSetup.call(this); - this.workspace_ = new Blockly.Workspace(); + this.workspace = new Blockly.Workspace(); }); teardown(function () { @@ -33,7 +33,7 @@ suite('JSON Block Definitions', function () { 'type': BLOCK_TYPE, }, ]); - block = new Blockly.Block(this.workspace_, BLOCK_TYPE); + block = new Blockly.Block(this.workspace, BLOCK_TYPE); }); assert.isNotNull(block); @@ -140,7 +140,7 @@ suite('JSON Block Definitions', function () { }, ]); - const block = new Blockly.Block(this.workspace_, BLOCK_TYPE); + const block = new Blockly.Block(this.workspace, BLOCK_TYPE); assert.equal(block.inputList.length, 1); assert.equal(block.inputList[0].fieldRow.length, 1); const textField = block.inputList[0].fieldRow[0]; @@ -161,7 +161,7 @@ suite('JSON Block Definitions', function () { }, ]); - const block = new Blockly.Block(this.workspace_, BLOCK_TYPE); + const block = new Blockly.Block(this.workspace, BLOCK_TYPE); assert.equal(block.inputList.length, 2); assert.equal(block.inputList[0].fieldRow.length, 1); @@ -189,7 +189,7 @@ suite('JSON Block Definitions', function () { }, ]); - const block = new Blockly.Block(this.workspace_, BLOCK_TYPE); + const block = new Blockly.Block(this.workspace, BLOCK_TYPE); assert.equal(block.inputList.length, 1); assert.equal(block.inputList[0].fieldRow.length, 1); const textField = block.inputList[0].fieldRow[0]; @@ -221,7 +221,7 @@ suite('JSON Block Definitions', function () { }, ]); - const block = new Blockly.Block(this.workspace_, BLOCK_TYPE); + const block = new Blockly.Block(this.workspace, BLOCK_TYPE); assert.equal(block.inputList.length, 1); assert.equal(block.inputList[0].fieldRow.length, 1); const dropdown = block.inputList[0].fieldRow[0]; @@ -274,7 +274,7 @@ suite('JSON Block Definitions', function () { }, ]); - const block = new Blockly.Block(this.workspace_, BLOCK_TYPE); + const block = new Blockly.Block(this.workspace, BLOCK_TYPE); assert.equal(block.inputList.length, 1); assert.equal(block.inputList[0].fieldRow.length, 1); const dropdown = block.inputList[0].fieldRow[0]; diff --git a/packages/blockly/tests/mocha/keyboard_movement_test.js b/packages/blockly/tests/mocha/keyboard_movement_test.js index 2d9c39ee299..afe753b5f8e 100644 --- a/packages/blockly/tests/mocha/keyboard_movement_test.js +++ b/packages/blockly/tests/mocha/keyboard_movement_test.js @@ -15,6 +15,7 @@ import { DEFAULT_INJECT_OPTIONS, sharedTestSetup, sharedTestTeardown, + workspaceTeardown, } from './test_helpers/setup_teardown.js'; import {createKeyDownEvent} from './test_helpers/user_input.js'; @@ -1148,6 +1149,10 @@ suite('Keyboard-driven movement', function () { ); }); + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + suite('Constrained moves of a simple reporter block', function () { setup(function () { appendBlock(this.workspace, VALUE_SIMPLE, 'join0', 'ADD0'); diff --git a/packages/blockly/tests/mocha/keyboard_navigation_test.js b/packages/blockly/tests/mocha/keyboard_navigation_test.js index 49179f5211c..077b9e95cc1 100644 --- a/packages/blockly/tests/mocha/keyboard_navigation_test.js +++ b/packages/blockly/tests/mocha/keyboard_navigation_test.js @@ -13,6 +13,7 @@ import { DEFAULT_INJECT_OPTIONS, sharedTestSetup, sharedTestTeardown, + workspaceTeardown, } from './test_helpers/setup_teardown.js'; import {createKeyDownEvent} from './test_helpers/user_input.js'; @@ -1145,6 +1146,10 @@ suite('Toolbox and flyout jump shortcuts (Ctrl/Cmd + Home / End)', function () { this.lastItem = allItems[allItems.length - 1]; }); + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + test('Navigable items exclude the separator', function () { const navigable = this.toolbox .getNavigator() @@ -1213,6 +1218,10 @@ suite('Toolbox and flyout jump shortcuts (Ctrl/Cmd + Home / End)', function () { ); }); + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + test('CtrlHome focuses the first item, which is a label rather than a block', function () { Blockly.getFocusManager().focusNode( this.flyoutWorkspace.getTopBlocks()[1], @@ -1329,6 +1338,10 @@ suite('Toolbox and flyout paging shortcuts (Page Up / Page Down)', function () { }; }); + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + test('PageDown focuses the last visible block', function () { this.layOutFlyout(0); Blockly.getFocusManager().focusNode(this.blocks[0]); @@ -1399,6 +1412,10 @@ suite('Toolbox and flyout paging shortcuts (Page Up / Page Down)', function () { }); }); + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + test('PageDown focuses the last visible category', function () { Blockly.getFocusManager().focusNode(this.items[0]); pressKey(this.workspace, Blockly.utils.KeyCodes.PAGE_DOWN); diff --git a/packages/blockly/tests/mocha/navigation_test.js b/packages/blockly/tests/mocha/navigation_test.js index 9e6cd307d43..5db3f3a77c6 100644 --- a/packages/blockly/tests/mocha/navigation_test.js +++ b/packages/blockly/tests/mocha/navigation_test.js @@ -702,9 +702,10 @@ suite('Navigation', function () { suite('In', function () { setup(function () { - const container = document.createElement('div'); - document.body.appendChild(container); - this.emptyWorkspace = Blockly.inject(container, DEFAULT_INJECT_OPTIONS); + this.emptyWorkspace = Blockly.inject( + 'blocklyDiv', + DEFAULT_INJECT_OPTIONS, + ); }); teardown(function () { workspaceTeardown.call(this, this.emptyWorkspace); diff --git a/packages/blockly/tests/mocha/render_management_test.js b/packages/blockly/tests/mocha/render_management_test.js index e30c79a4e0d..1839bf3dd58 100644 --- a/packages/blockly/tests/mocha/render_management_test.js +++ b/packages/blockly/tests/mocha/render_management_test.js @@ -8,6 +8,7 @@ import {assert} from 'chai'; import { sharedTestSetup, sharedTestTeardown, + workspaceTeardown, } from './test_helpers/setup_teardown.js'; suite('Render Management', function () { @@ -147,6 +148,10 @@ suite('Render Management', function () { ); }); + teardown(function () { + workspaceTeardown.call(this, this.workspace); + }); + test('does not record undo event when the render was queued with recordUndo disabled', function () { Blockly.Events.setRecordUndo(false); Blockly.renderManagement.queueRender(this.block); diff --git a/packages/blockly/tests/mocha/shortcut_items_test.js b/packages/blockly/tests/mocha/shortcut_items_test.js index 099f8fc521e..ef89476861a 100644 --- a/packages/blockly/tests/mocha/shortcut_items_test.js +++ b/packages/blockly/tests/mocha/shortcut_items_test.js @@ -57,9 +57,9 @@ suite('Keyboard Shortcut Items', function () { */ function setSelectedConnection(workspace) { const block = workspace.newBlock('stack_block'); - sinon - .stub(Blockly.getFocusManager(), 'getFocusedNode') - .returns(block.nextConnection); + block.initSvg(); + block.render(); + Blockly.getFocusManager().focusNode(block.nextConnection); } /** @@ -68,7 +68,7 @@ suite('Keyboard Shortcut Items', function () { */ function setSelectedComment(workspace) { const comment = workspace.newComment(); - sinon.stub(Blockly.getFocusManager(), 'getFocusedNode').returns(comment); + Blockly.getFocusManager().focusNode(comment); return comment; } @@ -117,6 +117,7 @@ suite('Keyboard Shortcut Items', function () { test('Called when connection is focused', function () { setSelectedConnection(this.workspace); + this.hideChaffSpy.resetHistory(); this.injectionDiv.dispatchEvent(this.event); sinon.assert.calledOnce(this.hideChaffSpy); }); @@ -217,6 +218,7 @@ suite('Keyboard Shortcut Items', function () { // Do not delete anything if a connection is focused. test('Not called when connection is focused', function () { setSelectedConnection(this.workspace); + this.hideChaffSpy.resetHistory(); const event = createKeyDownEvent(Blockly.utils.KeyCodes.DELETE); this.injectionDiv.dispatchEvent(event); sinon.assert.notCalled(this.hideChaffSpy); @@ -271,6 +273,7 @@ suite('Keyboard Shortcut Items', function () { }); test('Not called when connection is focused', function () { setSelectedConnection(this.workspace); + this.hideChaffSpy.resetHistory(); const event = createKeyDownEvent(Blockly.utils.KeyCodes.C, [ Blockly.utils.KeyCodes.CTRL, ]); @@ -282,6 +285,7 @@ suite('Keyboard Shortcut Items', function () { test('Workspace comment', function () { this.comment = setSelectedComment(this.workspace); this.copySpy = sinon.spy(this.comment, 'toCopyData'); + this.hideChaffSpy.resetHistory(); this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.calledOnce(this.copySpy); @@ -360,6 +364,7 @@ suite('Keyboard Shortcut Items', function () { }); test('Not called when connection is focused', function () { setSelectedConnection(this.workspace); + this.hideChaffSpy.resetHistory(); const event = createKeyDownEvent(Blockly.utils.KeyCodes.C, [ Blockly.utils.KeyCodes.CTRL, ]); diff --git a/packages/blockly/tests/mocha/test_helpers/workspace.js b/packages/blockly/tests/mocha/test_helpers/workspace.js index b3ed4da904d..e529e365e09 100644 --- a/packages/blockly/tests/mocha/test_helpers/workspace.js +++ b/packages/blockly/tests/mocha/test_helpers/workspace.js @@ -644,7 +644,9 @@ export function testAWorkspace() { }); teardown(function () { - workspaceTeardown.call(this, this.workspaceB); + if (this.workspaceB) { + workspaceTeardown.call(this, this.workspaceB); + } }); test('Trivial', function () { @@ -671,6 +673,7 @@ export function testAWorkspace() { test('After dispose', function () { this.workspaceB.dispose(); assert.isNull(Blockly.Workspace.getById(this.workspaceB.id)); + this.workspaceB = null; }); }); diff --git a/packages/blockly/tests/mocha/toolbox_test.js b/packages/blockly/tests/mocha/toolbox_test.js index 9da2c42a046..e8763219787 100644 --- a/packages/blockly/tests/mocha/toolbox_test.js +++ b/packages/blockly/tests/mocha/toolbox_test.js @@ -12,6 +12,7 @@ import { import { sharedTestSetup, sharedTestTeardown, + workspaceTeardown, } from './test_helpers/setup_teardown.js'; import { getBasicToolbox, @@ -29,11 +30,11 @@ suite('Toolbox', function () { setup(function () { sharedTestSetup.call(this); this.toolbox = getInjectedToolbox(); + this.workspace = this.toolbox.getWorkspace(); defineStackBlock(); }); teardown(function () { - this.toolbox.dispose(); sharedTestTeardown.call(this); }); @@ -700,6 +701,10 @@ suite('Toolbox', function () { metricsStub.returns({}); }); + teardown(function () { + workspaceTeardown.call(this, this.toolbox.getWorkspace()); + }); + function checkHorizontalToolbox(toolbox) { assert.equal(toolbox.HtmlDiv.style.left, '0px', 'Check left position'); assert.equal(toolbox.HtmlDiv.style.height, 'auto', 'Check height'); diff --git a/packages/blockly/tests/mocha/workspace_svg_test.js b/packages/blockly/tests/mocha/workspace_svg_test.js index 6fe2edbd7a7..ea67d730086 100644 --- a/packages/blockly/tests/mocha/workspace_svg_test.js +++ b/packages/blockly/tests/mocha/workspace_svg_test.js @@ -16,6 +16,7 @@ import { DEFAULT_INJECT_OPTIONS, sharedTestSetup, sharedTestTeardown, + workspaceTeardown, } from './test_helpers/setup_teardown.js'; import {dispatchPointerEvent} from './test_helpers/user_input.js'; import {testAWorkspace} from './test_helpers/workspace.js'; @@ -195,15 +196,15 @@ suite('WorkspaceSvg', function () { }); test('includes flyouts in nested trees', async function () { - this.workspace.dispose(); - const toolbox = document.getElementById('toolbox-simple'); - this.workspace = Blockly.inject('blocklyDiv', {toolbox: toolbox}); + const workspace = Blockly.inject('blocklyDiv', { + ...DEFAULT_INJECT_OPTIONS, + toolbox: document.getElementById('toolbox-simple'), + }); - const nestedTrees = this.workspace.getNestedTrees(); - assert.isNotNull(this.workspace.getFlyout()); - assert.sameMembers(nestedTrees, [ - this.workspace.getFlyout().getWorkspace(), - ]); + const nestedTrees = workspace.getNestedTrees(); + assert.isNotNull(workspace.getFlyout()); + assert.sameMembers(nestedTrees, [workspace.getFlyout().getWorkspace()]); + workspaceTeardown.call(this, workspace); }); }); @@ -229,6 +230,7 @@ suite('WorkspaceSvg', function () { sinon .stub(Blockly.utils.toolbox.TEST_ONLY, 'hasCategoriesInternal') .returns(true); + const originalToolbox = this.workspace.toolbox; this.workspace.toolbox = null; assert.throws( function () { @@ -236,6 +238,7 @@ suite('WorkspaceSvg', function () { }.bind(this), "Existing toolbox has no categories. Can't change mode.", ); + this.workspace.toolbox = originalToolbox; }); test('Existing toolbox has categories', function () { sinon From 37c1082fe79201bdb4cf0d3f393f15253124aef0 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 26 Aug 2026 12:52:39 -0700 Subject: [PATCH 2/3] fix: Fix leak of trashcan flyout --- packages/blockly/core/trashcan.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/blockly/core/trashcan.ts b/packages/blockly/core/trashcan.ts index 5f19ddc0abb..972aaa42a28 100644 --- a/packages/blockly/core/trashcan.ts +++ b/packages/blockly/core/trashcan.ts @@ -287,6 +287,8 @@ export class Trashcan */ dispose() { this.workspace.getComponentManager().removeComponent('trashcan'); + this.flyout?.dispose(); + this.flyout = null; if (this.svgGroup) { dom.removeNode(this.svgGroup); } From 76e3bf210698ea3a49326b0e4e2bd9b9b82116c9 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Wed, 26 Aug 2026 12:52:59 -0700 Subject: [PATCH 3/3] fix: Make `findHighlightSvg()` more robust --- packages/blockly/core/rendered_connection.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/blockly/core/rendered_connection.ts b/packages/blockly/core/rendered_connection.ts index 88a72b26ae5..a9423fd3bc3 100644 --- a/packages/blockly/core/rendered_connection.ts +++ b/packages/blockly/core/rendered_connection.ts @@ -746,11 +746,9 @@ export class RenderedConnection } private findHighlightSvg(): SVGPathElement | null { - // This cast is valid as TypeScript's definition is wrong. See: - // https://github.com/microsoft/TypeScript/issues/60996. - const root = this.getSourceBlock().getSvgRoot().getRootNode() as - ShadowRoot | HTMLDocument; - return root.getElementById(this.id) as SVGPathElement | null; + return this.getSourceBlock() + .getSvgRoot() + .querySelector(`#${CSS.escape(this.id)}`); } /**